<commit_before>/* eslint-env mocha */
var Logsene = require('../index.js')
var token = process.env.LOGSENE_TOKEN || 'YOUR_TEST_TOKEN'
process.env.LOGSENE_URL = 'http://127.0.0.1:19200/_bulk'
if (!process.env.LOGSENE_URL) {
  process.env.LOGSENE_URL = 'http://apps1.test.sematext.com:8088/_bulk'
}

console.log('Receiver: ' + process.env.LOGSENE_URL)
console.log('Token: ' + process.env.LOGSENE_TOKEN)

var http = require('http')
var httpStatusToReturn = 200
http.createServer(function (req, res) {
  res.writeHead(httpStatusToReturn, {'Content-Type': 'text/plain'})
  req.on('data', function (data) {
    // console.log(data.toString().substring(0,10))
  })
  var body = JSON.stringify({error: 'bad request', status: 400})
  if (httpStatusToReturn === 200)
    body = 'OK'
  res.end(body)
}).listen(19200, '127.0.0.1')

var MAX_MB = Number(process.env.LOAD_TEST_MAX_MB) || 35
describe('Logsene Load Test ', function () {
  it('memory keeps below ' + MAX_MB + ' MB since start', function (done) {
    this.timeout(120000)
    try {
      console.log('\t\tLOAD_TEST_MAX_MB: ' + MAX_MB)
      var logCount = Number(process.env.LOAD_TEST_SIZE) || 50000
      console.log('\t\tLOAD_TEST_SIZE: ' + logCount)

      var memory = process.memoryUsage().heapUsed
      var counter = 0

      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL, './')
      var start = new Date().getTime()
      var doneCalled = false
      console.log('\tRSS: ' + process.memoryUsage().rss / 1024 / 1024 + ' MB')
      console.log('\tHeap used: ' + process.memoryUsage().heapUsed / 1024 / 1024 + ' MB')

      function evtH (event) {
        if (doneCalled) {
          return
        }
        counter = counter + event.count
        var memory2 = 0
        if (counter >= logCount) {
          memory2 = process.memoryUsage().heapUsed
          var heapDiff = ((memory2 - memory) / 1024 / 1024)
          console.log('\tHeap diff: ' + heapDiff + ' MB')
          console.log('\tHeap used: ' + process.memoryUsage().heapUsed / 1024 / 1024 + ' MB')
          console.log('\tRSS: ' + process.memoryUsage().rss / 1024 / 1024 + ' MB')
          console.log('\tTransmission duration for ' + counter + ' logs: ' + (new Date().getTime() - start) / 1000 + ' sec.')
          if (heapDiff < MAX_MB) {
            doneCalled = true
            done()
          } else {
            done(new Error('Too much memory used:' + heapDiff + ' MB'))
          }
        }
      }

      logsene.on('log', evtH)
      logsene.once('error', function (event) {
        console.log(event)
        done(event)
      })
      logsene.on('error', console.log)
      for (var i = 0; i < logCount; i++) {
        logsene.log('info', 'test message ' + i, {testField: 'Test custom field ' + i, counter: i})
      }
    } catch (err) {
      done(err)
    }
  })
})

describe('Logsene constructor', function () {
  it('should fail without a token', function (done) {
    var emptyTokens = [undefined, null, '']
    emptyTokens.forEach(function (token) {
      try {
        new Logsene(token)
        done(new Error('Should throw exception'))
      } catch (err) {
        // nothing to do here
      }
    })
    done()
  })
})

describe('Logsene DiskBuffer ', function () {
  it('re-transmit', function (done) {
    this.timeout(120000)
    process.env.DEBUG_LOGSENE_DISK_BUFFER = true
    var DiskBuffer = require('../DiskBuffer.js')
    var db = DiskBuffer.createDiskBuffer({
      tmpDir: './tmp',
      interval: 1000
    })
    db.syncFileListFromDir()
    db.on('retransmit-req', function (event) {
      db.rmFile(event.fileName)
      db.retransmitNext()
    })
    db.once('removed', function () {
      done()
    })
    setTimeout(function () {
      db.store({message: 'hello'}, function (e,d) {
        db.retransmitNext()
      })
    }, 1000)
  })
})

describe('Logsene persistance ', function () {
  it('re-transmit', function (done) {
    this.timeout(70000)
    try {
      process.env.LOGSENE_DISK_BUFFER_INTERVAL = 2000
      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL, './mocha-test')
      var url = logsene.url
      logsene.diskBuffer(true, './mocha-test')
      logsene.setUrl('http://notreachable.test')
      logsene.db.once('removed', function (event) {
        done()
      })
      logsene.on('error', function (err) {
        if (err) {
          logsene.setUrl(url)
        }
      })
      setTimeout(function () {
        for (var i = 0; i <= 1001; i++) {
          logsene.log('info', 'test retransmit message ' + i, {_id: 'hey', testField: 'Test custom field ' + i, counter: i, _type: 'test_type', 'dot.sep.field': 34})
        }
      }, 1000)
    } catch (err) {
      done(err)
    }
  })
})

describe('Logsene log ', function () {
  it('should not throw circular reference error', function (done) {
    var logsene = new Logsene(token, 'test')
    logsene.on('error', console.log)
    function Foo () {
      this.abc = 'Hello'
      this.circular = this
    }
    var foo = new Foo()
    try {
      logsene.log('info', 'circular test', foo)
      if (logsene.bulkReq.getContentsAsString('utf-8').indexOf('[Circular') !== -1) {
        done()
      } else {
        done(new Error('The circular reference was not caught'))
      }
    } catch (err) {
      done(err)
    }
  })
  it('logs have default fields message, @timestamp, host, ip + custom fields', function (done) {
    this.timeout(20000)
    try {
      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL)
      // check for all required fields!
      logsene.once('logged', function (event) {
        if (!event.msg.testField || !event.msg.message || !event.msg['@timestamp'] || !event.msg.severity || !event.msg.host || !event.msg.ip) {
          done(new Error('missing fields in log: ' + JSON.stringify(event.msg)))
        } else {
          if (event.msg.message === 'test') {
            done()
          }
        }
      })
      logsene.once('error', function (event) {
        done(event)
      })
      logsene.log('info', 'test', {testField: 'Test custom field '})
    } catch (err) {
      done(err)
    }
  })
  it('leading _ in field names are removed, dots are replaced with _', function (done) {
    this.timeout(20000)
    try {
      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL)
      // check for all required fields!
      logsene.once('logged', function (event) {
        if (!event.msg.test_test) {
          done(new Error('field _test was not renamed: ' + JSON.stringify(event.msg)))
        } else {
          if (event.msg.test_test === 'test') {
            done()
          }
        }
      })
      logsene.once('error', function (event) {
        done(event)
      })
      logsene.log('info', 'test', {'_test.test': 'test'})
    } catch (err) {
      done(err)
    }
  })
  it('transmit', function (done) {
    this.timeout(20000)
    try {
      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL)
      // check for all required fields!
      logsene.on('logged', function (event) {
        if (!event.msg.message || !event.msg['@timestamp'] || !event.msg.severity || !event.msg.host || !event.msg.ip) {
          done(new Error('missing fields in log:' + JSON.stringify(event.msg)))
        }
      })
      logsene.once('log', function (event) {
        done()
      })
      logsene.once('error', function (event) {
        console.log(event)
        done(event)
      })
      logsene.on('error', console.log)
      for (var i = 0; i <= 100; i++) {
        logsene.log('info', 'test message ' + i, {testField: 'Test custom field ' + i, counter: i})
      }
    } catch (err) {
      done(err)
    }
  })
  it('transmit fail with status > 399 generates error event', function (done) {
    this.timeout(20000)
    try {
      httpStatusToReturn = 501
      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL)
      //logsene.once('log', function (event) {
        // this should not happen in this test case
        //done(event)
      //})

      logsene.once('error', function (event) {
        // this is the error event we expect
        // reset to 200 for next test ...
        httpStatusToReturn = 200
        if (event.err && event.err.message && event.err.httpStatus) {
          done()
        } else {
          done(new Error('missing message field and status code in error event'))
        }
        console.log('\t' + JSON.stringify(event.err))
      })
      logsene.log('info', 'test message')
      logsene.send()
    } catch (err) {
      done(err)
    }
  })
  it('transmit fail keeps flat memory footprint', function (done) {
    this.timeout(30000)
    try {
      var errorCounter = 0
      httpStatusToReturn = 501
      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL)
      var hu = process.memoryUsage().rss
      logsene.on('error', function (event) {
        //console.log(process.memoryUsage())
        diff = (process.memoryUsage().rss - hu)/1024/1024
        //console.log(hu/1024/1024)
        errorCounter++
        //console.log('\t' + errorCounter + ' ' + JSON.stringify(event.err))
        if (errorCounter >= 100000/1000) {
          if (diff < 200)
            done()
          else {
            console.log()
            done(new Error('too much memory used:' + diff + ' MB' + JSON.stringify(process.memoryUsage())))
          }
        }
      })
      for (var i=0; i<100000; i++)
      {
        logsene.log('info', 'test message')
      }
      logsene.send()
    } catch (err) {
      done(err)
    }
  })
})

<commit_msg>fix multiple call of done() in test<commit_after>/* eslint-env mocha */
var Logsene = require('../index.js')
var token = process.env.LOGSENE_TOKEN || 'YOUR_TEST_TOKEN'
process.env.LOGSENE_URL = 'http://127.0.0.1:19200/_bulk'
if (!process.env.LOGSENE_URL) {
  process.env.LOGSENE_URL = 'http://apps1.test.sematext.com:8088/_bulk'
}

console.log('Receiver: ' + process.env.LOGSENE_URL)
console.log('Token: ' + process.env.LOGSENE_TOKEN)

var http = require('http')
var httpStatusToReturn = 200
http.createServer(function (req, res) {
  res.writeHead(httpStatusToReturn, {'Content-Type': 'text/plain'})
  req.on('data', function (data) {
    // console.log(data.toString().substring(0,10))
  })
  var body = JSON.stringify({error: 'bad request', status: 400})
  if (httpStatusToReturn === 200)
    body = 'OK'
  res.end(body)
}).listen(19200, '127.0.0.1')

var MAX_MB = Number(process.env.LOAD_TEST_MAX_MB) || 35
describe('Logsene Load Test ', function () {
  it('memory keeps below ' + MAX_MB + ' MB since start', function (done) {
    this.timeout(120000)
    try {
      console.log('\t\tLOAD_TEST_MAX_MB: ' + MAX_MB)
      var logCount = Number(process.env.LOAD_TEST_SIZE) || 50000
      console.log('\t\tLOAD_TEST_SIZE: ' + logCount)

      var memory = process.memoryUsage().heapUsed
      var counter = 0

      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL, './')
      var start = new Date().getTime()
      var doneCalled = false
      console.log('\tRSS: ' + process.memoryUsage().rss / 1024 / 1024 + ' MB')
      console.log('\tHeap used: ' + process.memoryUsage().heapUsed / 1024 / 1024 + ' MB')

      function evtH (event) {
        if (doneCalled) {
          return
        }
        counter = counter + event.count
        var memory2 = 0
        if (counter >= logCount) {
          memory2 = process.memoryUsage().heapUsed
          var heapDiff = ((memory2 - memory) / 1024 / 1024)
          console.log('\tHeap diff: ' + heapDiff + ' MB')
          console.log('\tHeap used: ' + process.memoryUsage().heapUsed / 1024 / 1024 + ' MB')
          console.log('\tRSS: ' + process.memoryUsage().rss / 1024 / 1024 + ' MB')
          console.log('\tTransmission duration for ' + counter + ' logs: ' + (new Date().getTime() - start) / 1000 + ' sec.')
          if (heapDiff < MAX_MB) {
            doneCalled = true
            done()
          } else {
            done(new Error('Too much memory used:' + heapDiff + ' MB'))
          }
        }
      }

      logsene.on('log', evtH)
      logsene.once('error', function (event) {
        console.log(event)
        done(event)
      })
      logsene.on('error', console.log)
      for (var i = 0; i < logCount; i++) {
        logsene.log('info', 'test message ' + i, {testField: 'Test custom field ' + i, counter: i})
      }
    } catch (err) {
      done(err)
    }
  })
})

describe('Logsene constructor', function () {
  it('should fail without a token', function (done) {
    var emptyTokens = [undefined, null, '']
    emptyTokens.forEach(function (token) {
      try {
        new Logsene(token)
        done(new Error('Should throw exception'))
      } catch (err) {
        // nothing to do here
      }
    })
    done()
  })
})

describe('Logsene DiskBuffer ', function () {
  it('re-transmit', function (done) {
    this.timeout(120000)
    process.env.DEBUG_LOGSENE_DISK_BUFFER = true
    var DiskBuffer = require('../DiskBuffer.js')
    var db = DiskBuffer.createDiskBuffer({
      tmpDir: './tmp',
      interval: 1000
    })
    db.syncFileListFromDir()
    db.on('retransmit-req', function (event) {
      db.rmFile(event.fileName)
      db.retransmitNext()
    })
    db.once('removed', function () {
      done()
    })
    setTimeout(function () {
      db.store({message: 'hello'}, function (e,d) {
        db.retransmitNext()
      })
    }, 1000)
  })
})

describe('Logsene persistance ', function () {
  it('re-transmit', function (done) {
    this.timeout(70000)
    try {
      process.env.LOGSENE_DISK_BUFFER_INTERVAL = 2000
      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL, './mocha-test')
      var url = logsene.url
      logsene.diskBuffer(true, './mocha-test')
      logsene.setUrl('http://notreachable.test')
      logsene.db.once('removed', function (event) {
        done()
      })
      logsene.on('error', function (err) {
        if (err) {
          logsene.setUrl(url)
        }
      })
      setTimeout(function () {
        for (var i = 0; i <= 1001; i++) {
          logsene.log('info', 'test retransmit message ' + i, {_id: 'hey', testField: 'Test custom field ' + i, counter: i, _type: 'test_type', 'dot.sep.field': 34})
        }
      }, 1000)
    } catch (err) {
      done(err)
    }
  })
})

describe('Logsene log ', function () {
  it('should not throw circular reference error', function (done) {
    var logsene = new Logsene(token, 'test')
    logsene.on('error', console.log)
    function Foo () {
      this.abc = 'Hello'
      this.circular = this
    }
    var foo = new Foo()
    try {
      logsene.log('info', 'circular test', foo)
      if (logsene.bulkReq.getContentsAsString('utf-8').indexOf('[Circular') !== -1) {
        done()
      } else {
        done(new Error('The circular reference was not caught'))
      }
    } catch (err) {
      done(err)
    }
  })
  it('logs have default fields message, @timestamp, host, ip + custom fields', function (done) {
    this.timeout(20000)
    try {
      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL)
      // check for all required fields!
      logsene.once('logged', function (event) {
        if (!event.msg.testField || !event.msg.message || !event.msg['@timestamp'] || !event.msg.severity || !event.msg.host || !event.msg.ip) {
          done(new Error('missing fields in log: ' + JSON.stringify(event.msg)))
        } else {
          if (event.msg.message === 'test') {
            done()
          }
        }
      })
      logsene.once('error', function (event) {
        done(event)
      })
      logsene.log('info', 'test', {testField: 'Test custom field '})
    } catch (err) {
      done(err)
    }
  })
  it('leading _ in field names are removed, dots are replaced with _', function (done) {
    this.timeout(20000)
    try {
      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL)
      // check for all required fields!
      logsene.once('logged', function (event) {
        if (!event.msg.test_test) {
          done(new Error('field _test was not renamed: ' + JSON.stringify(event.msg)))
        } else {
          if (event.msg.test_test === 'test') {
            done()
          }
        }
      })
      logsene.once('error', function (event) {
        done(event)
      })
      logsene.log('info', 'test', {'_test.test': 'test'})
    } catch (err) {
      done(err)
    }
  })
  it('transmit', function (done) {
    this.timeout(20000)
    try {
      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL)
      // check for all required fields!
      logsene.on('logged', function (event) {
        if (!event.msg.message || !event.msg['@timestamp'] || !event.msg.severity || !event.msg.host || !event.msg.ip) {
          done(new Error('missing fields in log:' + JSON.stringify(event.msg)))
        }
      })
      logsene.once('log', function (event) {
        done()
      })
      logsene.once('error', function (event) {
        console.log(event)
        done(event)
      })
      logsene.on('error', console.log)
      for (var i = 0; i <= 100; i++) {
        logsene.log('info', 'test message ' + i, {testField: 'Test custom field ' + i, counter: i})
      }
    } catch (err) {
      done(err)
    }
  })
  it('transmit fail with status > 399 generates error event', function (done) {
    this.timeout(20000)
    try {
      httpStatusToReturn = 501
      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL)
      //logsene.once('log', function (event) {
        // this should not happen in this test case
        //done(event)
      //})

      logsene.once('error', function (event) {
        // this is the error event we expect
        // reset to 200 for next test ...
        httpStatusToReturn = 200
        if (event.err && event.err.message && event.err.httpStatus) {
          done()
        } else {
          done(new Error('missing message field and status code in error event'))
        }
        console.log('\t' + JSON.stringify(event.err))
      })
      logsene.log('info', 'test message')
      logsene.send()
    } catch (err) {
      done(err)
    }
  })
  it('transmit fail keeps flat memory footprint', function (done) {
    this.timeout(30000)
    try {
      var errorCounter = 0
      httpStatusToReturn = 501
      var logsene = new Logsene(token, 'test', process.env.LOGSENE_URL)
      var hu = process.memoryUsage().rss
      logsene.on('error', function (event) {
        //console.log(process.memoryUsage())
        diff = (process.memoryUsage().rss - hu)/1024/1024
        //console.log(hu/1024/1024)
        errorCounter++
        //console.log('\t' + errorCounter + ' ' + JSON.stringify(event.err))
        if (errorCounter >= 100000/1000) {
          errorCounter=0
          if (diff < 200)
            done()
          else {
            console.log()
            done(new Error('too much memory used:' + diff + ' MB' + JSON.stringify(process.memoryUsage())))
          }
        }
      })
      for (var i=0; i<100000; i++)
      {
        logsene.log('info', 'test message')
      }
      logsene.send()
    } catch (err) {
      done(err)
    }
  })
})

<|endoftext|>


<commit_before>'use strict';

const cluster = require('cluster');
const DgramSocket = require('dgram').Socket;
const { EventEmitter } = require('events');
const { Socket, Server } = require('net');
const ChildProcess = require('child_process').ChildProcess;

const bunyan = require('bunyan');

const Status = require('./status');
const removeValue = require('./remove-value');
const prependListener = require('./prepend-listener');

let isInstantiated = false;

// Server status listener
const serverStatusEvent = new EventEmitter();
serverStatusEvent.fetch = () => {
    if (!cluster.isMaster) process.send({ event: 'serverStatus' });
};

function trackResponseEnd(request, response) {
    function changeStatus(state) {
        if (request.flora.state === 'aborted') {
            // remove connection (parent) from status as it's already closed:
            request.flora.status.parent.close();
        } else {
            request.flora.state = state;
        }
    }

    // inject our "end" method to track "sending" status:
    response._floraOrigEnd = response.end;
    response.end = (...args) => {
        changeStatus('sending');
        return response._floraOrigEnd(...args);
    };

    // track if someone pipes to us (no "end" call is guaranteed then):
    response.once('pipe', () => changeStatus('piping'));
}

/**
 * Cluster worker.
 */
class Worker extends EventEmitter {
    /**
     * Constructor.
     *
     * * Options:
     * - shutdownTimeout: worker shutdown timeout (default: 30000)
     * - log: bunyan instance for logging (default: new bunyan logger)
     * - httpServer: http.Server instance (optional)
     *
     * @param {Object} [opts]
     */
    constructor(opts = {}) {
        super();

        this.config = opts;

        this.config.shutdownTimeout = this.config.shutdownTimeout || 30000;

        if (this.config.log) {
            this.log = opts.log.child({ component: 'worker' });
        } else {
            this.log = bunyan.createLogger({ name: 'flora-cluster', component: 'worker' });
        }

        this.httpServer = null;
        this.isRunning = true;
        this.activeConnections = [];

        this.status = new Status();
        this.status.set('state', 'initializing');
        this.status.set('startTime', new Date());
        this.status.set('pid', process.pid);
        this.status.set('generation', null); // just for nicer order - set later in master
        this.status.onStatus(() => {
            this.status.set('state', this.isRunning ? 'running' : 'shutdown');
            this.status.set('memoryUsage', process.memoryUsage());
        });

        if (isInstantiated) this.log.warn('Worker should be instantiated only once per process');
        isInstantiated = true;

        process.on('message', (message) => {
            if (!message.event) return;
            this.log.trace(message, 'message from master');
            process.emit(`flora::${message.event}`, message);
        });

        process.on('flora::shutdown', this.shutdown.bind(this));

        process.on('flora::status', this.sendStatus.bind(this));

        process.on('flora::serverStatus', (message) => {
            serverStatusEvent.emit('serverStatus', message.serverStatus);
        });

        process.on('SIGTERM', () => {
            this.log.info('received SIGTERM, exiting immediately');
            process.exit(0);
        });

        process.on('SIGINT', () => {
            this.log.info('received SIGINT, exiting immediately');
            process.exit(0);
        });

        process.on('SIGQUIT', () => {
            this.log.info('received SIGQUIT, shutting down gracefully');
            this.shutdown();
        });

        process.on('uncaughtException', (err) => {
            this.log.error(err, 'uncaught exception, shutting down gracefully');
            process.exitCode = 1;
            this.shutdown();
        });

        process.on('unhandledRejection', (err /* , promise */) => {
            this.log.error(err, 'unhandled rejection, shutting down gracefully');
            process.exitCode = 1;
            this.shutdown();
        });

        if (this.config.httpServer) {
            this.attach(this.config.httpServer);
        }
    }

    /**
     * Tell the master that the worker is ready.
     *
     * This function needs to be called before `startupTimeout` is over, otherwise the worker
     * is assumed to failed to start and thus being killed by the master process.
     */
    ready() {
        if (!cluster.isMaster) {
            this.log.debug('sending "ready" event to master');
            this.send({ event: 'ready' });
        }
    }

    /**
     * Attach to an exising HTTP server.
     *
     * This may be necessary after creating the Worker instance, so we can instantiate the
     * http.Server later.
     *
     * @param {http.Server} server
     */
    attach(server) {
        this.httpServer = server;
        prependListener(this.httpServer, 'request', this.onRequest.bind(this));
        prependListener(this.httpServer, 'connection', this.onConnection.bind(this));
        this.httpServer.on('close', this.onCloseServer.bind(this));
    }

    /**
     * Shutdown the worker
     */
    shutdown() {
        if (!this.isRunning) return;
        this.isRunning = false;

        if (!this.httpServer) {
            if (!cluster.isMaster) cluster.worker.disconnect();
            return;
        }

        try {
            // make sure we close down within given timeout:
            const shutdownTimeout = setTimeout(() => {
                this.logActiveHandles();
                this.log.warn('exit after timeout');
                setTimeout(() => {
                    process.exit(2);
                }, 100); // give async logging a chance before hard exit
            }, this.config.shutdownTimeout);

            // but don't keep the process open just for that:
            shutdownTimeout.unref();

            // notify master that we are shutting down (so new workers can be spawned immediately):
            if (!cluster.isMaster) this.send({ event: 'shutdown' });

            this.emit('close');

            // close keep-alive-connection (and do not wait for FIN ACK from client):
            this.activeConnections.forEach((connection) => {
                if (connection.flora.isIdle) connection.destroy();
            });

            this.sendStatus();
        } catch (e) {
            this.log.error(e, 'error on graceful shutdown');
        }
    }

    logActiveHandles() {
        if (typeof process._getActiveHandles !== 'function') {
            this.log.debug("Can't check for active handles - process._getActiveHandles not available");
            return;
        }

        const whitelist = [];
        if (process.stdout) whitelist.push(process.stdout);
        if (process.stderr) whitelist.push(process.stderr);
        if (cluster.worker && cluster.worker.suicide && process._channel) {
            whitelist.push(process._channel); // cluster IPC pipe
        }

        function dumpObjectProperties(obj) {
            const properties = [];

            do {
                const name = (obj.constructor && obj.constructor.name) || '(Unknown)';
                properties.push(name + '[' + Object.getOwnPropertyNames(obj).join(', ') + ']');
            } while ((obj = Object.getPrototypeOf(obj)) && obj !== Object.prototype);

            return properties.join(' ~ ');
        }

        const handles = process._getActiveHandles(); // internal node interface!

        const handleInfos = [];
        handles.forEach((handle) => {
            let name = typeof handle;
            let description = null;

            if (whitelist.indexOf(handle) !== -1) return;

            if (handle === null) {
                name = 'null-handle';
                description = 'wtf?!';
            } else if (handle && typeof handle === 'object') {
                if (handle.constructor && handle.constructor.name) {
                    name = handle.constructor.name;
                }

                if (handle instanceof Socket) {
                    if (handle.remoteAddress) {
                        description = handle.localAddress + ':' + handle.localPort;
                        description += ' -> ' + handle.remoteAddress + ':' + handle.remotePort;
                    } else {
                        description = dumpObjectProperties(handle);
                    }
                    description += handle.destroyed ? ' (destroyed)' : '';
                } else if (handle instanceof DgramSocket) {
                    name = 'dgram.Socket';
                    description = 'type: ' + handle.type;
                } else if (handle instanceof Server) {
                    const { address, port } = handle.address();
                    description = `${address}:${port}`;
                } else if (handle instanceof ChildProcess) {
                    description = 'PID: ' + handle.pid;
                    description += '; args: ' + (handle.spawnargs ? handle.spawnargs.join(' ') : '-');
                } else {
                    description = dumpObjectProperties(handle);
                }
            }

            handleInfos.push({ name, description });
        });

        handleInfos.forEach((handleInfo) => {
            this.log.warn(
                `Active handle: ${handleInfo.name}` + (handleInfo.description ? ` (${handleInfo.description})` : '')
            );
        });
    }

    onRequest(request, response) {
        request.flora = request.flora || {};
        request.flora.startTime = process.hrtime();
        request.flora.state = 'processing';

        request.connection.flora.isIdle = false;
        request.connection.flora.status.increment('requests');
        request.connection.flora.status.set('idleSince', null);

        const status = request.connection.flora.status.child('request');
        request.flora.status = status;

        status.set('state', null);
        status.set('startTime', new Date());
        status.onStatus(() => {
            status.set('state', request.flora.state);
            status.set('method', request.method);
            status.set('url', request.url);
            status.set('httpVersion', request.httpVersion);
        });

        // TODO: Set different timeout for running requests than for idle keep-alive-connections

        // emulate response.on('end') - when finished processing the request:
        trackResponseEnd(request, response);

        response.on('finish', () => {
            request.flora.state = 'finished';
            status.close();
            request.connection.flora.isIdle = true;
            request.connection.flora.status.set('idleSince', new Date());
            if (request.connection.flora.isClosed) {
                request.connection.flora.status.close();
            }

            if (!this.isRunning) {
                // close keep-alive-connection (and do not wait for FIN ACK from clients):
                request.connection.destroy();
            }
        });

        response.on('close', () => {
            if (request.flora.state !== 'processing') {
                status.close();
                if (request.connection.flora.isClosed) {
                    request.connection.flora.status.close();
                }
            }

            request.flora.state = 'aborted';
        });
    }

    onConnection(connection) {
        connection.setNoDelay(true);
        connection.setKeepAlive(true);

        connection.flora = connection.flora || {};
        connection.flora.isIdle = true;

        this.status.increment('connections');

        const status = this.status.addChild('activeConnections');
        connection.flora.status = status;

        status.set('state', 'connected');
        status.set('startTime', new Date());
        status.set('remoteAddress', connection.remoteAddress);
        status.set('remotePort', connection.remotePort);
        status.set('idleSince', new Date());
        status.onStatus(() => {
            status.setIncrement('bytesRead', connection.bytesRead);
            status.setIncrement('bytesWritten', connection.bytesWritten);
        });

        this.activeConnections.push(connection);

        connection.on('close', () => {
            status.setIncrement('bytesRead', connection.bytesRead);
            status.setIncrement('bytesWritten', connection.bytesWritten);
            status.set('state', 'closed');
            if (connection.flora.isIdle) {
                status.close();
            } else {
                connection.flora.isClosed = true;
            }
            removeValue(this.activeConnections, connection);
        });
    }

    onCloseServer() {
        this.sendStatus();
        this.log.info('Closing IPC channel - should exit now');

        // if all references are closed properly, worker exits here,
        // otherwise the kill-timeout from shutdown() should catch it:
        if (!cluster.isMaster) cluster.worker.disconnect();
    }

    sendStatus() {
        if (!cluster.isMaster) this.send({ event: 'status', status: this.status.getStatus() });
    }

    send(message) {
        process.send(message, (err) => {
            if (err) {
                // catch IPC channel errors:
                this.log.error(err, 'error sending IPC message to master');
            }
        });
    }

    /**
     * Retrieve the cluster status
     *
     * @returns {Promise}
     */
    async serverStatus() {
        if (cluster.isMaster) return this.status.getStatus();

        return new Promise((resolve) => {
            serverStatusEvent.fetch();
            serverStatusEvent.once('serverStatus', (status) => resolve(status));
        });
    }
}

module.exports = Worker;
<commit_msg>worker: expose request.ip in status (if available)<commit_after>'use strict';

const cluster = require('cluster');
const DgramSocket = require('dgram').Socket;
const { EventEmitter } = require('events');
const { Socket, Server } = require('net');
const ChildProcess = require('child_process').ChildProcess;

const bunyan = require('bunyan');

const Status = require('./status');
const removeValue = require('./remove-value');
const prependListener = require('./prepend-listener');

let isInstantiated = false;

// Server status listener
const serverStatusEvent = new EventEmitter();
serverStatusEvent.fetch = () => {
    if (!cluster.isMaster) process.send({ event: 'serverStatus' });
};

function trackResponseEnd(request, response) {
    function changeStatus(state) {
        if (request.flora.state === 'aborted') {
            // remove connection (parent) from status as it's already closed:
            request.flora.status.parent.close();
        } else {
            request.flora.state = state;
        }
    }

    // inject our "end" method to track "sending" status:
    response._floraOrigEnd = response.end;
    response.end = (...args) => {
        changeStatus('sending');
        return response._floraOrigEnd(...args);
    };

    // track if someone pipes to us (no "end" call is guaranteed then):
    response.once('pipe', () => changeStatus('piping'));
}

/**
 * Cluster worker.
 */
class Worker extends EventEmitter {
    /**
     * Constructor.
     *
     * * Options:
     * - shutdownTimeout: worker shutdown timeout (default: 30000)
     * - log: bunyan instance for logging (default: new bunyan logger)
     * - httpServer: http.Server instance (optional)
     *
     * @param {Object} [opts]
     */
    constructor(opts = {}) {
        super();

        this.config = opts;

        this.config.shutdownTimeout = this.config.shutdownTimeout || 30000;

        if (this.config.log) {
            this.log = opts.log.child({ component: 'worker' });
        } else {
            this.log = bunyan.createLogger({ name: 'flora-cluster', component: 'worker' });
        }

        this.httpServer = null;
        this.isRunning = true;
        this.activeConnections = [];

        this.status = new Status();
        this.status.set('state', 'initializing');
        this.status.set('startTime', new Date());
        this.status.set('pid', process.pid);
        this.status.set('generation', null); // just for nicer order - set later in master
        this.status.onStatus(() => {
            this.status.set('state', this.isRunning ? 'running' : 'shutdown');
            this.status.set('memoryUsage', process.memoryUsage());
        });

        if (isInstantiated) this.log.warn('Worker should be instantiated only once per process');
        isInstantiated = true;

        process.on('message', (message) => {
            if (!message.event) return;
            this.log.trace(message, 'message from master');
            process.emit(`flora::${message.event}`, message);
        });

        process.on('flora::shutdown', this.shutdown.bind(this));

        process.on('flora::status', this.sendStatus.bind(this));

        process.on('flora::serverStatus', (message) => {
            serverStatusEvent.emit('serverStatus', message.serverStatus);
        });

        process.on('SIGTERM', () => {
            this.log.info('received SIGTERM, exiting immediately');
            process.exit(0);
        });

        process.on('SIGINT', () => {
            this.log.info('received SIGINT, exiting immediately');
            process.exit(0);
        });

        process.on('SIGQUIT', () => {
            this.log.info('received SIGQUIT, shutting down gracefully');
            this.shutdown();
        });

        process.on('uncaughtException', (err) => {
            this.log.error(err, 'uncaught exception, shutting down gracefully');
            process.exitCode = 1;
            this.shutdown();
        });

        process.on('unhandledRejection', (err /* , promise */) => {
            this.log.error(err, 'unhandled rejection, shutting down gracefully');
            process.exitCode = 1;
            this.shutdown();
        });

        if (this.config.httpServer) {
            this.attach(this.config.httpServer);
        }
    }

    /**
     * Tell the master that the worker is ready.
     *
     * This function needs to be called before `startupTimeout` is over, otherwise the worker
     * is assumed to failed to start and thus being killed by the master process.
     */
    ready() {
        if (!cluster.isMaster) {
            this.log.debug('sending "ready" event to master');
            this.send({ event: 'ready' });
        }
    }

    /**
     * Attach to an exising HTTP server.
     *
     * This may be necessary after creating the Worker instance, so we can instantiate the
     * http.Server later.
     *
     * @param {http.Server} server
     */
    attach(server) {
        this.httpServer = server;
        prependListener(this.httpServer, 'request', this.onRequest.bind(this));
        prependListener(this.httpServer, 'connection', this.onConnection.bind(this));
        this.httpServer.on('close', this.onCloseServer.bind(this));
    }

    /**
     * Shutdown the worker
     */
    shutdown() {
        if (!this.isRunning) return;
        this.isRunning = false;

        if (!this.httpServer) {
            if (!cluster.isMaster) cluster.worker.disconnect();
            return;
        }

        try {
            // make sure we close down within given timeout:
            const shutdownTimeout = setTimeout(() => {
                this.logActiveHandles();
                this.log.warn('exit after timeout');
                setTimeout(() => {
                    process.exit(2);
                }, 100); // give async logging a chance before hard exit
            }, this.config.shutdownTimeout);

            // but don't keep the process open just for that:
            shutdownTimeout.unref();

            // notify master that we are shutting down (so new workers can be spawned immediately):
            if (!cluster.isMaster) this.send({ event: 'shutdown' });

            this.emit('close');

            // close keep-alive-connection (and do not wait for FIN ACK from client):
            this.activeConnections.forEach((connection) => {
                if (connection.flora.isIdle) connection.destroy();
            });

            this.sendStatus();
        } catch (e) {
            this.log.error(e, 'error on graceful shutdown');
        }
    }

    logActiveHandles() {
        if (typeof process._getActiveHandles !== 'function') {
            this.log.debug("Can't check for active handles - process._getActiveHandles not available");
            return;
        }

        const whitelist = [];
        if (process.stdout) whitelist.push(process.stdout);
        if (process.stderr) whitelist.push(process.stderr);
        if (cluster.worker && cluster.worker.suicide && process._channel) {
            whitelist.push(process._channel); // cluster IPC pipe
        }

        function dumpObjectProperties(obj) {
            const properties = [];

            do {
                const name = (obj.constructor && obj.constructor.name) || '(Unknown)';
                properties.push(name + '[' + Object.getOwnPropertyNames(obj).join(', ') + ']');
            } while ((obj = Object.getPrototypeOf(obj)) && obj !== Object.prototype);

            return properties.join(' ~ ');
        }

        const handles = process._getActiveHandles(); // internal node interface!

        const handleInfos = [];
        handles.forEach((handle) => {
            let name = typeof handle;
            let description = null;

            if (whitelist.indexOf(handle) !== -1) return;

            if (handle === null) {
                name = 'null-handle';
                description = 'wtf?!';
            } else if (handle && typeof handle === 'object') {
                if (handle.constructor && handle.constructor.name) {
                    name = handle.constructor.name;
                }

                if (handle instanceof Socket) {
                    if (handle.remoteAddress) {
                        description = handle.localAddress + ':' + handle.localPort;
                        description += ' -> ' + handle.remoteAddress + ':' + handle.remotePort;
                    } else {
                        description = dumpObjectProperties(handle);
                    }
                    description += handle.destroyed ? ' (destroyed)' : '';
                } else if (handle instanceof DgramSocket) {
                    name = 'dgram.Socket';
                    description = 'type: ' + handle.type;
                } else if (handle instanceof Server) {
                    const { address, port } = handle.address();
                    description = `${address}:${port}`;
                } else if (handle instanceof ChildProcess) {
                    description = 'PID: ' + handle.pid;
                    description += '; args: ' + (handle.spawnargs ? handle.spawnargs.join(' ') : '-');
                } else {
                    description = dumpObjectProperties(handle);
                }
            }

            handleInfos.push({ name, description });
        });

        handleInfos.forEach((handleInfo) => {
            this.log.warn(
                `Active handle: ${handleInfo.name}` + (handleInfo.description ? ` (${handleInfo.description})` : '')
            );
        });
    }

    onRequest(request, response) {
        request.flora = request.flora || {};
        request.flora.startTime = process.hrtime();
        request.flora.state = 'processing';

        request.connection.flora.isIdle = false;
        request.connection.flora.status.increment('requests');
        request.connection.flora.status.set('idleSince', null);

        const status = request.connection.flora.status.child('request');
        request.flora.status = status;

        status.set('state', null);
        status.set('startTime', new Date());
        status.onStatus(() => {
            status.set('state', request.flora.state);
            status.set('ip', request.ip || (request.connection ? request.connection.remoteAddress : null));
            status.set('method', request.method);
            status.set('url', request.url);
            status.set('httpVersion', request.httpVersion);
        });

        // TODO: Set different timeout for running requests than for idle keep-alive-connections

        // emulate response.on('end') - when finished processing the request:
        trackResponseEnd(request, response);

        response.on('finish', () => {
            request.flora.state = 'finished';
            status.close();
            request.connection.flora.isIdle = true;
            request.connection.flora.status.set('idleSince', new Date());
            if (request.connection.flora.isClosed) {
                request.connection.flora.status.close();
            }

            if (!this.isRunning) {
                // close keep-alive-connection (and do not wait for FIN ACK from clients):
                request.connection.destroy();
            }
        });

        response.on('close', () => {
            if (request.flora.state !== 'processing') {
                status.close();
                if (request.connection.flora.isClosed) {
                    request.connection.flora.status.close();
                }
            }

            request.flora.state = 'aborted';
        });
    }

    onConnection(connection) {
        connection.setNoDelay(true);
        connection.setKeepAlive(true);

        connection.flora = connection.flora || {};
        connection.flora.isIdle = true;

        this.status.increment('connections');

        const status = this.status.addChild('activeConnections');
        connection.flora.status = status;

        status.set('state', 'connected');
        status.set('startTime', new Date());
        status.set('remoteAddress', connection.remoteAddress);
        status.set('remotePort', connection.remotePort);
        status.set('idleSince', new Date());
        status.onStatus(() => {
            status.setIncrement('bytesRead', connection.bytesRead);
            status.setIncrement('bytesWritten', connection.bytesWritten);
        });

        this.activeConnections.push(connection);

        connection.on('close', () => {
            status.setIncrement('bytesRead', connection.bytesRead);
            status.setIncrement('bytesWritten', connection.bytesWritten);
            status.set('state', 'closed');
            if (connection.flora.isIdle) {
                status.close();
            } else {
                connection.flora.isClosed = true;
            }
            removeValue(this.activeConnections, connection);
        });
    }

    onCloseServer() {
        this.sendStatus();
        this.log.info('Closing IPC channel - should exit now');

        // if all references are closed properly, worker exits here,
        // otherwise the kill-timeout from shutdown() should catch it:
        if (!cluster.isMaster) cluster.worker.disconnect();
    }

    sendStatus() {
        if (!cluster.isMaster) this.send({ event: 'status', status: this.status.getStatus() });
    }

    send(message) {
        process.send(message, (err) => {
            if (err) {
                // catch IPC channel errors:
                this.log.error(err, 'error sending IPC message to master');
            }
        });
    }

    /**
     * Retrieve the cluster status
     *
     * @returns {Promise}
     */
    async serverStatus() {
        if (cluster.isMaster) return this.status.getStatus();

        return new Promise((resolve) => {
            serverStatusEvent.fetch();
            serverStatusEvent.once('serverStatus', (status) => resolve(status));
        });
    }
}

module.exports = Worker;
<|endoftext|>


<commit_before>import RSAA from './RSAA';
import { isRSAA, validateRSAA } from './validation';
import { InvalidRSAA, RequestError, InternalError } from './errors';
import { normalizeTypeDescriptors, actionWith } from './util';

/**
 * Default options for redux-api-middleware
 * These can be customized by passing options into `createMiddleware`
 * @type {Object}
 */
const defaults = {
  ok: res => res.ok,
  fetch
};

/**
 * A middleware creator used to create a ReduxApiMiddleware
 * with custom defaults
 *
 * @type {function}
 * @returns {ReduxMiddleware}
 * @access public
 */
function createMiddleware(options = {}) {
  const middlewareOptions = Object.assign({}, defaults, options);

  return ({ getState }) => next => action => {
    // Do not process actions without an [RSAA] property
    if (!isRSAA(action)) {
      return next(action);
    }

    return (async () => {
      // Try to dispatch an error request FSA for invalid RSAAs
      const validationErrors = validateRSAA(action);
      if (validationErrors.length) {
        const callAPI = action[RSAA];
        if (callAPI.types && Array.isArray(callAPI.types)) {
          let requestType = callAPI.types[0];
          if (requestType && requestType.type) {
            requestType = requestType.type;
          }
          next({
            type: requestType,
            payload: new InvalidRSAA(validationErrors),
            error: true
          });
        }
        return;
      }

      // Parse the validated RSAA action
      const callAPI = action[RSAA];
      var {
        endpoint,
        body,
        headers,
        options = {},
        fetch: doFetch = middlewareOptions.fetch,
        ok = middlewareOptions.ok
      } = callAPI;
      const { method, credentials, bailout, types } = callAPI;
      const [requestType, successType, failureType] = normalizeTypeDescriptors(
        types
      );

      // Should we bail out?
      try {
        if (
          (typeof bailout === 'boolean' && bailout) ||
          (typeof bailout === 'function' && bailout(getState()))
        ) {
          return;
        }
      } catch (e) {
        return next(
          await actionWith(
            {
              ...failureType,
              payload: new RequestError('[RSAA].bailout function failed'),
              error: true
            },
            [action, getState()]
          )
        );
      }

      // Process [RSAA].endpoint function
      if (typeof endpoint === 'function') {
        try {
          endpoint = await endpoint(getState());
        } catch (e) {
          return next(
            await actionWith(
              {
                ...failureType,
                payload: new RequestError('[RSAA].endpoint function failed'),
                error: true
              },
              [action, getState()]
            )
          );
        }
      }

      // Process [RSAA].body function
      if (typeof body === 'function') {
        try {
          body = await body(getState());
        } catch (e) {
          return next(
            await actionWith(
              {
                ...failureType,
                payload: new RequestError('[RSAA].body function failed'),
                error: true
              },
              [action, getState()]
            )
          );
        }
      }

      // Process [RSAA].headers function
      if (typeof headers === 'function') {
        try {
          headers = await headers(getState());
        } catch (e) {
          return next(
            await actionWith(
              {
                ...failureType,
                payload: new RequestError('[RSAA].headers function failed'),
                error: true
              },
              [action, getState()]
            )
          );
        }
      }

      // Process [RSAA].options function
      if (typeof options === 'function') {
        try {
          options = await options(getState());
        } catch (e) {
          return next(
            await actionWith(
              {
                ...failureType,
                payload: new RequestError('[RSAA].options function failed'),
                error: true
              },
              [action, getState()]
            )
          );
        }
      }

      // We can now dispatch the request FSA
      if (
        typeof requestType.payload === 'function' ||
        typeof requestType.meta === 'function'
      ) {
        next(await actionWith(requestType, [action, getState()]));
      } else {
        next(requestType);
      }

      let res;
      try {
        // Make the API call
        res = await doFetch(endpoint, {
          ...options,
          method,
          body: body || undefined,
          credentials,
          headers: headers || {}
        });
      } catch (e) {
        // The request was malformed, or there was a network error
        return next(
          await actionWith(
            {
              ...failureType,
              payload: new RequestError(e.message),
              error: true
            },
            [action, getState()]
          )
        );
      }

      let isOk;
      try {
        isOk = ok(res);
      } catch (e) {
        return next(
          await actionWith(
            {
              ...failureType,
              payload: new InternalError('[RSAA].ok function failed'),
              error: true
            },
            [action, getState(), res]
          )
        );
      }

      // Process the server response
      if (isOk) {
        return next(await actionWith(successType, [action, getState(), res]));
      } else {
        return next(
          await actionWith(
            {
              ...failureType,
              error: true
            },
            [action, getState(), res]
          )
        );
      }
    })();
  };
}

/**
 * A Redux middleware that processes RSAA actions.
 *
 * @type {ReduxMiddleware}
 * @access public
 */
function apiMiddleware({ getState }) {
  return createMiddleware()({ getState });
}

export { createMiddleware, apiMiddleware };
<commit_msg>fix(middleware): use global fetch as last resort<commit_after>import RSAA from './RSAA';
import { isRSAA, validateRSAA } from './validation';
import { InvalidRSAA, RequestError, InternalError } from './errors';
import { normalizeTypeDescriptors, actionWith } from './util';

/**
 * Default options for redux-api-middleware
 * These can be customized by passing options into `createMiddleware`
 * @type {Object}
 */
const defaults = {
  ok: res => res.ok
};

/**
 * A middleware creator used to create a ReduxApiMiddleware
 * with custom defaults
 *
 * @type {function}
 * @returns {ReduxMiddleware}
 * @access public
 */
function createMiddleware(options = {}) {
  const middlewareOptions = Object.assign({}, defaults, options);

  return ({ getState }) => next => action => {
    // Do not process actions without an [RSAA] property
    if (!isRSAA(action)) {
      return next(action);
    }

    return (async () => {
      // Try to dispatch an error request FSA for invalid RSAAs
      const validationErrors = validateRSAA(action);
      if (validationErrors.length) {
        const callAPI = action[RSAA];
        if (callAPI.types && Array.isArray(callAPI.types)) {
          let requestType = callAPI.types[0];
          if (requestType && requestType.type) {
            requestType = requestType.type;
          }
          next({
            type: requestType,
            payload: new InvalidRSAA(validationErrors),
            error: true
          });
        }
        return;
      }

      // Parse the validated RSAA action
      const callAPI = action[RSAA];
      var {
        endpoint,
        body,
        headers,
        options = {},
        fetch: doFetch = middlewareOptions.fetch || fetch,
        ok = middlewareOptions.ok
      } = callAPI;
      const { method, credentials, bailout, types } = callAPI;
      const [requestType, successType, failureType] = normalizeTypeDescriptors(
        types
      );

      // Should we bail out?
      try {
        if (
          (typeof bailout === 'boolean' && bailout) ||
          (typeof bailout === 'function' && bailout(getState()))
        ) {
          return;
        }
      } catch (e) {
        return next(
          await actionWith(
            {
              ...failureType,
              payload: new RequestError('[RSAA].bailout function failed'),
              error: true
            },
            [action, getState()]
          )
        );
      }

      // Process [RSAA].endpoint function
      if (typeof endpoint === 'function') {
        try {
          endpoint = await endpoint(getState());
        } catch (e) {
          return next(
            await actionWith(
              {
                ...failureType,
                payload: new RequestError('[RSAA].endpoint function failed'),
                error: true
              },
              [action, getState()]
            )
          );
        }
      }

      // Process [RSAA].body function
      if (typeof body === 'function') {
        try {
          body = await body(getState());
        } catch (e) {
          return next(
            await actionWith(
              {
                ...failureType,
                payload: new RequestError('[RSAA].body function failed'),
                error: true
              },
              [action, getState()]
            )
          );
        }
      }

      // Process [RSAA].headers function
      if (typeof headers === 'function') {
        try {
          headers = await headers(getState());
        } catch (e) {
          return next(
            await actionWith(
              {
                ...failureType,
                payload: new RequestError('[RSAA].headers function failed'),
                error: true
              },
              [action, getState()]
            )
          );
        }
      }

      // Process [RSAA].options function
      if (typeof options === 'function') {
        try {
          options = await options(getState());
        } catch (e) {
          return next(
            await actionWith(
              {
                ...failureType,
                payload: new RequestError('[RSAA].options function failed'),
                error: true
              },
              [action, getState()]
            )
          );
        }
      }

      // We can now dispatch the request FSA
      if (
        typeof requestType.payload === 'function' ||
        typeof requestType.meta === 'function'
      ) {
        next(await actionWith(requestType, [action, getState()]));
      } else {
        next(requestType);
      }

      let res;
      try {
        // Make the API call
        res = await doFetch(endpoint, {
          ...options,
          method,
          body: body || undefined,
          credentials,
          headers: headers || {}
        });
      } catch (e) {
        // The request was malformed, or there was a network error
        return next(
          await actionWith(
            {
              ...failureType,
              payload: new RequestError(e.message),
              error: true
            },
            [action, getState()]
          )
        );
      }

      let isOk;
      try {
        isOk = ok(res);
      } catch (e) {
        return next(
          await actionWith(
            {
              ...failureType,
              payload: new InternalError('[RSAA].ok function failed'),
              error: true
            },
            [action, getState(), res]
          )
        );
      }

      // Process the server response
      if (isOk) {
        return next(await actionWith(successType, [action, getState(), res]));
      } else {
        return next(
          await actionWith(
            {
              ...failureType,
              error: true
            },
            [action, getState(), res]
          )
        );
      }
    })();
  };
}

/**
 * A Redux middleware that processes RSAA actions.
 *
 * @type {ReduxMiddleware}
 * @access public
 * @deprecated since v3.2.0 use `createMiddleware`
 */
function apiMiddleware({ getState }) {
  return createMiddleware()({ getState });
}

export { createMiddleware, apiMiddleware };
<|endoftext|>


<commit_before>'use strict';

const crypto = require('crypto'),
    N = require('./HttpHeaderNames'),
    EntityType = require('./../Constants').StorageEntityType,
    etag = require('./../utils'),
    InternalAzuriteError = require('./../InternalAzuriteError');

class AzuriteRequest {
    constructor({
        req = null,
        entityType = undefined,
        payload = undefined }) {

        if (req === null) {
            throw new InternalAzuriteError('AzuriteRequest: req cannot be null!');
        }

        this.httpProps = {};
        this.metaProps = {};
        this.body = req.body;
        this.entityType = entityType;
        this.query = req.query;
        this.now = Date.now();
        this.payload = payload;
        this._initMetaProps(req.rawHeaders);
        this._initHttpProps(req.headers);
    }

    leaseId() {
        return this.httpProps[N.LEASE_ID];
    }

    // Working on rawHeaders for meta attributes to preserve casing.
    _initMetaProps(rawHeaders) {
        this.metaProps = rawHeaders.map((e, i, a) => {
            if (e.indexOf('x-ms-meta-') !== -1) {
                e = e.replace('x-ms-meta-', '');
                const o = {};
                o[e] = a[i + 1];
                return o;
            }
        }).filter((e) => {
            return e !== undefined;
        }).reduce((acc, e) => {
            const key = Object.keys(e)[0];
            acc[key] = e[key];
            return acc;
        }, {});
    }

    _initHttpProps(httpHeaders) {
        this.httpProps[N.CONTENT_LENGTH] = httpHeaders['Content-Length'] || httpHeaders['content-length'];
        // x-ms-* attributes have precedence over according HTTP-Headers
        this.httpProps[N.CONTENT_TYPE] = httpHeaders['x-ms-blob-content-type'] || httpHeaders['content-type'] || 'application/octet-stream';
        this.httpProps[N.CONTENT_ENCODING] = httpHeaders['x-ms-blob-content-encoding'] || httpHeaders['content-encoding'];
        this.httpProps[N.CONTENT_DISPOSITION] = httpHeaders['x-ms-blob-content-disposition'] || httpHeaders['content-disposition'];
        this.httpProps[N.CACHE_CONTROL] = httpHeaders['x-ms-blob-cache-control'] || httpHeaders['cache-control'];
        this.httpProps[N.CONTENT_LANGUAGE] = httpHeaders['x-ms-blob-content-language'] || httpHeaders['content-language'];
        this.httpProps[N.CONTENT_MD5] = httpHeaders['x-ms-blob-content-md5'] || httpHeaders['content-md5'];
        this.httpProps[N.RANGE] = httpHeaders['x-ms-range'] || httpHeaders['range'];

        this.httpProps[N.BLOB_TYPE] = httpHeaders['x-ms-blob-type']
        this.httpProps[N.RANGE_GET_CONTENT_MD5] = httpHeaders['x-ms-range-get-content-md5'];
        this.httpProps[N.DELETE_SNAPSHOTS] = httpHeaders['x-ms-delete-snapshots'];
        this.httpProps[N.LEASE_ID] = httpHeaders['x-ms-lease-id'];
        this.httpProps[N.LEASE_ACTION] = httpHeaders['x-ms-lease-action'];
        this.httpProps[N.LEASE_DURATION] = httpHeaders['x-ms-lease-duration'];
        this.httpProps[N.LEASE_BREAK_PERIOD] = httpHeaders['x-ms-lease-break-period'];
        this.httpProps[N.PROPOSED_LEASE_ID] = httpHeaders['x-ms-proposed-lease-id'];
        this.httpProps[N.IF_MODFIFIED_SINCE] = httpHeaders['if-modified-since'];
        this.httpProps[N.IF_UNMODIFIED_SINCE] = httpHeaders['if-unmodified-since'];
        this.httpProps[N.IF_MATCH] = httpHeaders['if-match'];
        this.httpProps[N.IF_NONE_MATCH] = httpHeaders['if-none-match'];
        // As per spec @ https://docs.microsoft.com/en-us/rest/api/storageservices/set-container-acl
        // if this header is not specified it is set to 'private' per default.
        this.httpProps[N.BLOB_PUBLIC_ACCESS] = httpHeaders['x-ms-blob-public-access'] || 'private';
        // Append Blobs specific
        this.httpProps[N.BLOB_CONDITION_MAX_SIZE] = parseInt(httpHeaders['x-ms-blob-condition-maxsize']) || undefined;
        this.httpProps[N.BLOB_CONDITION_APPENDPOS] = parseInt(httpHeaders['x-ms-blob-condition-appendpos']) || undefined;
        // Page Blobs specific
        this.httpProps[N.BLOB_CONTENT_LENGTH] = httpHeaders['x-ms-blob-content-length'];
        this.httpProps[N.PAGE_WRITE] = httpHeaders['x-ms-page-write'];
        this.httpProps[N.IF_SEQUENCE_NUMBER_LE] = httpHeaders['x-ms-if-sequence-number-le'];
        this.httpProps[N.IF_SEQUENCE_NUMBER_LT] = httpHeaders['x-ms-if-sequence-number-lt'];
        this.httpProps[N.IF_SEQUENCE_NUMBER_EQ] = httpHeaders['x-ms-if-sequence-number-eq'];

        Object.keys(this.httpProps).forEach((key) => {
            if (this.httpProps[key] === undefined) {
                delete this.httpProps[key];
            }
        });
    }
}

module.exports = AzuriteRequest;<commit_msg>fixes<commit_after>'use strict';

const crypto = require('crypto'),
    N = require('./HttpHeaderNames'),
    EntityType = require('./../Constants').StorageEntityType,
    etag = require('./../utils'),
    InternalAzuriteError = require('./../InternalAzuriteError');

class AzuriteRequest {
    constructor({
        req = null,
        entityType = undefined,
        payload = undefined }) {

        if (req === null) {
            throw new InternalAzuriteError('AzuriteRequest: req cannot be null!');
        }

        this.httpProps = {};
        this.metaProps = {};
        this.body = req.body;
        this.entityType = entityType;
        this.query = req.query;
        this.now = Date.now();
        this.payload = payload;
        this._initMetaProps(req.rawHeaders);
        this._initHttpProps(req.headers);
    }

    leaseId() {
        return this.httpProps[N.LEASE_ID];
    }

    // Working on rawHeaders for meta attributes to preserve casing.
    _initMetaProps(rawHeaders) {
        this.metaProps = rawHeaders.map((e, i, a) => {
            if (e.indexOf('x-ms-meta-') !== -1) {
                e = e.replace('x-ms-meta-', '');
                const o = {};
                o[e] = a[i + 1];
                return o;
            }
        }).filter((e) => {
            return e !== undefined;
        }).reduce((acc, e) => {
            const key = Object.keys(e)[0];
            acc[key] = e[key];
            return acc;
        }, {});
    }

    _initHttpProps(httpHeaders) {
        this.httpProps[N.CONTENT_LENGTH] = httpHeaders['Content-Length'] || httpHeaders['content-length'];
        // x-ms-* attributes have precedence over according HTTP-Headers
        this.httpProps[N.CONTENT_TYPE] = httpHeaders['x-ms-blob-content-type'] || httpHeaders['content-type'] || 'application/octet-stream';
        this.httpProps[N.CONTENT_ENCODING] = httpHeaders['x-ms-blob-content-encoding'] || httpHeaders['content-encoding'];
        this.httpProps[N.CONTENT_DISPOSITION] = httpHeaders['x-ms-blob-content-disposition'] || httpHeaders['content-disposition'];
        this.httpProps[N.CACHE_CONTROL] = httpHeaders['x-ms-blob-cache-control'] || httpHeaders['cache-control'];
        this.httpProps[N.CONTENT_LANGUAGE] = httpHeaders['x-ms-blob-content-language'] || httpHeaders['content-language'];
        this.httpProps[N.CONTENT_MD5] = httpHeaders['x-ms-blob-content-md5'] || httpHeaders['content-md5'];
        this.httpProps[N.RANGE] = httpHeaders['x-ms-range'] || httpHeaders['range'];

        this.httpProps[N.BLOB_TYPE] = httpHeaders['x-ms-blob-type']
        this.httpProps[N.RANGE_GET_CONTENT_MD5] = httpHeaders['x-ms-range-get-content-md5'];
        this.httpProps[N.DELETE_SNAPSHOTS] = httpHeaders['x-ms-delete-snapshots'];
        this.httpProps[N.LEASE_ID] = httpHeaders['x-ms-lease-id'];
        this.httpProps[N.LEASE_ACTION] = httpHeaders['x-ms-lease-action'];
        this.httpProps[N.LEASE_DURATION] = httpHeaders['x-ms-lease-duration'];
        this.httpProps[N.LEASE_BREAK_PERIOD] = httpHeaders['x-ms-lease-break-period'];
        this.httpProps[N.PROPOSED_LEASE_ID] = httpHeaders['x-ms-proposed-lease-id'];
        this.httpProps[N.IF_MODFIFIED_SINCE] = httpHeaders['if-modified-since'];
        this.httpProps[N.IF_UNMODIFIED_SINCE] = httpHeaders['if-unmodified-since'];
        this.httpProps[N.IF_MATCH] = httpHeaders['if-match'];
        this.httpProps[N.IF_NONE_MATCH] = httpHeaders['if-none-match'];
        // As per spec @ https://docs.microsoft.com/en-us/rest/api/storageservices/set-container-acl
        // if this header is not specified it is set to 'private' per default.
        this.httpProps[N.BLOB_PUBLIC_ACCESS] = httpHeaders['x-ms-blob-public-access'] || 'private';
        // Append Blobs specific
        this.httpProps[N.BLOB_CONDITION_MAX_SIZE] = parseInt(httpHeaders['x-ms-blob-condition-maxsize']) || undefined;
        this.httpProps[N.BLOB_CONDITION_APPENDPOS] = parseInt(httpHeaders['x-ms-blob-condition-appendpos']) || undefined;
        // Page Blobs specific
        this.httpProps[N.BLOB_CONTENT_LENGTH] = httpHeaders['x-ms-blob-content-length'];
        this.httpProps[N.PAGE_WRITE] = httpHeaders['x-ms-page-write'];
        this.httpProps[N.IF_SEQUENCE_NUMBER_LE] = httpHeaders['x-ms-if-sequence-number-le'];
        this.httpProps[N.IF_SEQUENCE_NUMBER_LT] = httpHeaders['x-ms-if-sequence-number-lt'];
        this.httpProps[N.IF_SEQUENCE_NUMBER_EQ] = httpHeaders['x-ms-if-sequence-number-eq'];

        Object.keys(this.httpProps).forEach((key) => {
            if (this.httpProps[key] === undefined || this.httpProps[key] === 'undefined') {
                delete this.httpProps[key];
            }
        });
    }
}

module.exports = AzuriteRequest;<|endoftext|>


<commit_before>var should = require('should');

var liferay;
var config;

try {
  liferay = require('liferay-connector');
  config = require('config');
}
catch (e) {
  liferay = require('..');
  config = require('./config');
}

var Promise = liferay.Promise;

var TIMEOUT = 10e3;

var PORTAL_URL = config.PORTAL_URL;
var AUTH = config.AUTH;
var WRONG_AUTH = config.WRONG_AUTH;

var connector;
var connection;
var userGroup;

describe("The camelcase#normalize util", function () {
  var known = {
    // Known to be preserved
    "somethingUrl": "somethingUrl",
    // Known to be mangled
    "classPK": "classPk",
    "feedURL": "feedUrl",
    "entryURL": "entryUrl",
    "homeURL": "homeUrl",
    "smallImageURL": "smallImageUrl",
    "newClassPK": "newClassPk",
    "contentURL": "contentUrl",
    "friendlyURL": "friendlyUrl",
    "articleURL": "articleUrl",
    "displayDateGT": "displayDateGt",
    "displayDateLT": "displayDateLt",
    "friendlyURLMap": "friendlyUrlMap",
    "inUseSSL": "inUseSsl",
    "outUseSSL": "outUseSsl",
    "permissionClassPK": "permissionClassPk",
    "inputStreamOVPs": "inputStreamOvPs",
    "pageURL": "pageUrl",
    "downloadPageURL": "downloadPageUrl",
    "directDownloadURL": "directDownloadUrl",
    "testDirectDownloadURL": "testDirectDownloadUrl",
    "mediumImageURL": "mediumImageUrl",
    "largeImageURL": "largeImageUrl",
    "overrideClassPK": "overrideClassPk",
    "attachmentURLPrefix": "attachmentUrlPrefix"
  };

  it("should mangle correctly the known affected parameter names", function () {
    Object.keys(known).forEach(function (from) {
      var to = known[ from ];
      liferay.camelcase.normalize(from).should.eql(to);
    });
  });

  it("should be idempotent", function () {
    Object.keys(known).forEach(function (from) {
      var to = known[ from ];
      liferay.camelcase.normalize(to).should.eql(to);
    });
  });
});

describe("The camelcase.normalizeParameter util", function () {
  it("should preserve inner parameters", function () {
    liferay.camelcase.normalizeParameter("fullURL.fullURL").should.eql("fullUrl.fullURL");
  });
});

describe("The Connector#mangleCamelCase util", function () {
  it("should preserve nested service calls", function () {
    liferay.base.prototype.mangleCamelCase({
      "$user = /user/get-user-by-id": {
        "fullURL": 123,
        "userId": 123,
        "$contact = /contact/get-contact-by-id": {
          "fullURL": 123,
          "@contactId" : "$user.contactId"
        }
      }
    }).should.eql({
      "$user = /user/get-user-by-id": {
        "fullUrl": 123,
        "userId": 123,
        "$contact = /contact/get-contact-by-id": {
          "fullUrl": 123,
          "@contactId" : "$user.contactId"
        }
      }
    });
  });

  it("should preserve inner parameters", function () {
    liferay.base.prototype.mangleCamelCase({
      "/some/path": {
        "+fullURL": "java.util.Something",
        "fullURL.fullURL": 123
      }
    }).should.eql({
      "/some/path": {
        "+fullUrl": "java.util.Something",
        "fullUrl.fullURL": 123
      }
    });
  });
});

describe("The global connector", function () {

  this.timeout(TIMEOUT);

  it("should be able to indentify a Liferay (≥6.1)", function () {
    return liferay.identify(PORTAL_URL, AUTH).then(function (conn) {
      connector = conn;
    });
  });
});

describe("The given connector", function () {

  this.timeout(TIMEOUT);

  it("should give a valid connector", function () {
    connector.should.have.a.property('authenticate');
  });

  it("should authenticate correctly", function () {
    return connector.authenticate(PORTAL_URL, AUTH).then(function (conn) {
      connection = conn;
    });
  });

  it("should not authenticate with wrong credentials", function () {
    return connector.authenticate(PORTAL_URL, WRONG_AUTH).then(function () {
      throw new Error("The portal authenticated with wrong credentials!");
    })
    .catch(liferay.errors.Unauthorized, function (err) {
      return null;
    });
  });

  testConnectionQuality(1);
});

describe("Automatic authentication", function () {

  this.timeout(TIMEOUT);

  it("should authenticate without an identification", function () {
    return liferay.authenticate(PORTAL_URL, AUTH).then(function (conn) {
      connection = conn;
    });
  });

  testConnectionQuality(2);
});

function testConnectionQuality(count) {
  it("should have a few interesting properties (" + count + ")", function () {
    connection.should.have.a.property('user');
    connection.should.have.a.property('companyId');
    connection.companyId.should.be.greaterThan(0);
    connection.user.should.have.a.property('userId');
  });

  it("should have a few interesting methods (" + count + ")", function () {
    connection.should.have.a.property('invoke');
    connection.should.have.a.property('raw');

    connection.invoke.should.be.a.Function;
    connection.raw.should.be.a.Function;
  });
}

describe("Error assimilation", function () {
  it("should not get an MBMessage by a random id", function () {
    return connection.invoke({
      "/mbmessage/get-message": {
        messageId: 99999999
      }
    })
    .then(function (message) {
      throw new Error("Got a message with random id");
    })
    .catch(liferay.errors.NotFound, function (err) {
      return null;
    });
  });

  it("should understand wrong services", function () {
    return connection.invoke({
      "/i-do-not-exists/neither-i": {}
    })
    .then(function () {
      throw new Error("Resolved an non-existent service");
    })
    .catch(liferay.errors.BadRequest, function (err) {
      return null;
    });
  });
});

describe("Blogs services", function () {
  it("(with the user group)", function () {
    var candidates = connection.sites.filter(function (group) {
      return group.friendlyURL === '/' + connection.user.screenName;
    });

    candidates.should.have.length(1);

    userGroup = candidates[0];

    userGroup.should.have.a.property('groupId');
    userGroup.should.have.a.property('companyId');
  });

  var entry;

  it("should be able to create (valid) entries", function () {
    var now = new Date();
    return connection.invoke({
      "/blogsentry/add-entry": {
        title: 'title-' + Math.random(),
        description: 'description-' + Math.random(),
        content: 'content-' + Math.random(),
        displayDateMonth: now.getMonth(),
        displayDateDay: now.getDate(),
        displayDateYear: now.getFullYear(),
        displayDateHour: now.getHours(),
        displayDateMinute: now.getMinutes(),
        allowPingbacks: false,
        allowTrackbacks: false,
        trackbacks: [],
        smallImage: false,
        smallImageUrl: null,
        // Issue #5
        /*smallImageURL: null,*/
        smallImageFileName: null,
        smallImageInputStream: null,
        '+serviceContext': 'com.liferay.portal.service.ServiceContext',
        'serviceContext.companyId': userGroup.companyId,
        'serviceContext.scopeGroupId': userGroup.groupId,
        'serviceContext.addGuestPermissions': true,
        'serviceContext.addGroupPermissions': true
        // Issue #4
        /*serviceContext: {
          companyId: userGroup.groupId,
          scopeGroupId: userGroup.companyId,
          addGuestPermissions: true,
          addGroupPermissions: true
        }*/
      }
    })
    .then(function (result) {
      entry = result;

      entry.should.have.a.property('entryId');
      entry.should.have.a.property('title');
      entry.should.have.a.property('description');
      entry.should.have.a.property('content');

      entry.entryId.should.be.a.Number;
    });
  });

  it("should get that same entry", function () {
    return connection.invoke({
      "/blogsentry/get-entry": {
        entryId: entry.entryId
      }
    })
    .then(function (result) {
      result.should.eql(entry);
    });
  });

  it("should delete a fresh entry", function () {
    return connection.invoke({
      "/blogsentry/delete-entry": {
        entryId: entry.entryId
      }
    });
  });
});

if (config.TEST_COMPANION_PLUGIN) describe("Smoke tests (companion)", function () {
  it("should reach the companion (1/2)", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/smoke-test": {
        input: true
      }
    })
    .then(function (output) {
      output.should.be.eql(false);
    });
  });

  it("should reach the companion (2/2)", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/smoke-test": {
        input: false
      }
    })
    .then(function (output) {
      output.should.be.eql(true);
    });
  });
});

if (config.TEST_COMPANION_PLUGIN) describe("Return types (companion)", function () {
  it("should support JSONObject", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/json-object-test": {
        property: 'answer',
        value: '42'
      }
    })
    .then(function (jsonObject) {
      jsonObject.should.have.a.property("answer");
      jsonObject.answer.should.be.eql("42");
    });
  });

  it("should support User", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/current-user": {}
    })
    .then(function (user) {
      user.should.have.a.property('userId');
    })
  });
});

if (config.TEST_COMPANION_PLUGIN) describe("Service Context (companion)", function () {
  it("should support ServiceContext parameters", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/service-context-smoke-test": {
        currentUrl: 'nonsense',
        scopeGroupId: userGroup.groupId,
        '+serviceContext': 'com.liferay.portal.service.ServiceContext',
        'serviceContext.scopeGroupId': userGroup.groupId,
        'serviceContext.currentURL': 'nonsense'
      }
    })
    .then(function (ok) {
      ok.should.be.eql(true);
    });
  });
});

if (config.TEST_COMPANION_PLUGIN) describe("Parameter names (companion)", function () {
  it("should support manually mangled parameters", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/parameter-names-test": {
        somethingUrl: 'abc',
        somethingOvPs: 'def'
      }
    })
    .then(function (ok) {
      ok.should.be.eql(true);
    });
  });

  it("should support not-mangled parameters", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/parameter-names-test": {
        somethingURL: 'abc',
        somethingOVPs: 'def'
      }
    })
    .then(function (ok) {
      ok.should.be.eql(true);
    });
  });
});
<commit_msg>Use `.true` instead of `.eql(true)` in the specs<commit_after>var should = require('should');

var liferay;
var config;

try {
  liferay = require('liferay-connector');
  config = require('config');
}
catch (e) {
  liferay = require('..');
  config = require('./config');
}

var Promise = liferay.Promise;

var TIMEOUT = 10e3;

var PORTAL_URL = config.PORTAL_URL;
var AUTH = config.AUTH;
var WRONG_AUTH = config.WRONG_AUTH;

var connector;
var connection;
var userGroup;

describe("The camelcase#normalize util", function () {
  var known = {
    // Known to be preserved
    "somethingUrl": "somethingUrl",
    // Known to be mangled
    "classPK": "classPk",
    "feedURL": "feedUrl",
    "entryURL": "entryUrl",
    "homeURL": "homeUrl",
    "smallImageURL": "smallImageUrl",
    "newClassPK": "newClassPk",
    "contentURL": "contentUrl",
    "friendlyURL": "friendlyUrl",
    "articleURL": "articleUrl",
    "displayDateGT": "displayDateGt",
    "displayDateLT": "displayDateLt",
    "friendlyURLMap": "friendlyUrlMap",
    "inUseSSL": "inUseSsl",
    "outUseSSL": "outUseSsl",
    "permissionClassPK": "permissionClassPk",
    "inputStreamOVPs": "inputStreamOvPs",
    "pageURL": "pageUrl",
    "downloadPageURL": "downloadPageUrl",
    "directDownloadURL": "directDownloadUrl",
    "testDirectDownloadURL": "testDirectDownloadUrl",
    "mediumImageURL": "mediumImageUrl",
    "largeImageURL": "largeImageUrl",
    "overrideClassPK": "overrideClassPk",
    "attachmentURLPrefix": "attachmentUrlPrefix"
  };

  it("should mangle correctly the known affected parameter names", function () {
    Object.keys(known).forEach(function (from) {
      var to = known[ from ];
      liferay.camelcase.normalize(from).should.eql(to);
    });
  });

  it("should be idempotent", function () {
    Object.keys(known).forEach(function (from) {
      var to = known[ from ];
      liferay.camelcase.normalize(to).should.eql(to);
    });
  });
});

describe("The camelcase.normalizeParameter util", function () {
  it("should preserve inner parameters", function () {
    liferay.camelcase.normalizeParameter("fullURL.fullURL").should.eql("fullUrl.fullURL");
  });
});

describe("The Connector#mangleCamelCase util", function () {
  it("should preserve nested service calls", function () {
    liferay.base.prototype.mangleCamelCase({
      "$user = /user/get-user-by-id": {
        "fullURL": 123,
        "userId": 123,
        "$contact = /contact/get-contact-by-id": {
          "fullURL": 123,
          "@contactId" : "$user.contactId"
        }
      }
    }).should.eql({
      "$user = /user/get-user-by-id": {
        "fullUrl": 123,
        "userId": 123,
        "$contact = /contact/get-contact-by-id": {
          "fullUrl": 123,
          "@contactId" : "$user.contactId"
        }
      }
    });
  });

  it("should preserve inner parameters", function () {
    liferay.base.prototype.mangleCamelCase({
      "/some/path": {
        "+fullURL": "java.util.Something",
        "fullURL.fullURL": 123
      }
    }).should.eql({
      "/some/path": {
        "+fullUrl": "java.util.Something",
        "fullUrl.fullURL": 123
      }
    });
  });
});

describe("The global connector", function () {

  this.timeout(TIMEOUT);

  it("should be able to indentify a Liferay (≥6.1)", function () {
    return liferay.identify(PORTAL_URL, AUTH).then(function (conn) {
      connector = conn;
    });
  });
});

describe("The given connector", function () {

  this.timeout(TIMEOUT);

  it("should give a valid connector", function () {
    connector.should.have.a.property('authenticate');
  });

  it("should authenticate correctly", function () {
    return connector.authenticate(PORTAL_URL, AUTH).then(function (conn) {
      connection = conn;
    });
  });

  it("should not authenticate with wrong credentials", function () {
    return connector.authenticate(PORTAL_URL, WRONG_AUTH).then(function () {
      throw new Error("The portal authenticated with wrong credentials!");
    })
    .catch(liferay.errors.Unauthorized, function (err) {
      return null;
    });
  });

  testConnectionQuality(1);
});

describe("Automatic authentication", function () {

  this.timeout(TIMEOUT);

  it("should authenticate without an identification", function () {
    return liferay.authenticate(PORTAL_URL, AUTH).then(function (conn) {
      connection = conn;
    });
  });

  testConnectionQuality(2);
});

function testConnectionQuality(count) {
  it("should have a few interesting properties (" + count + ")", function () {
    connection.should.have.a.property('user');
    connection.should.have.a.property('companyId');
    connection.companyId.should.be.greaterThan(0);
    connection.user.should.have.a.property('userId');
  });

  it("should have a few interesting methods (" + count + ")", function () {
    connection.should.have.a.property('invoke');
    connection.should.have.a.property('raw');

    connection.invoke.should.be.a.Function;
    connection.raw.should.be.a.Function;
  });
}

describe("Error assimilation", function () {
  it("should not get an MBMessage by a random id", function () {
    return connection.invoke({
      "/mbmessage/get-message": {
        messageId: 99999999
      }
    })
    .then(function (message) {
      throw new Error("Got a message with random id");
    })
    .catch(liferay.errors.NotFound, function (err) {
      return null;
    });
  });

  it("should understand wrong services", function () {
    return connection.invoke({
      "/i-do-not-exists/neither-i": {}
    })
    .then(function () {
      throw new Error("Resolved an non-existent service");
    })
    .catch(liferay.errors.BadRequest, function (err) {
      return null;
    });
  });
});

describe("Blogs services", function () {
  it("(with the user group)", function () {
    var candidates = connection.sites.filter(function (group) {
      return group.friendlyURL === '/' + connection.user.screenName;
    });

    candidates.should.have.length(1);

    userGroup = candidates[0];

    userGroup.should.have.a.property('groupId');
    userGroup.should.have.a.property('companyId');
  });

  var entry;

  it("should be able to create (valid) entries", function () {
    var now = new Date();
    return connection.invoke({
      "/blogsentry/add-entry": {
        title: 'title-' + Math.random(),
        description: 'description-' + Math.random(),
        content: 'content-' + Math.random(),
        displayDateMonth: now.getMonth(),
        displayDateDay: now.getDate(),
        displayDateYear: now.getFullYear(),
        displayDateHour: now.getHours(),
        displayDateMinute: now.getMinutes(),
        allowPingbacks: false,
        allowTrackbacks: false,
        trackbacks: [],
        smallImage: false,
        smallImageUrl: null,
        // Issue #5
        /*smallImageURL: null,*/
        smallImageFileName: null,
        smallImageInputStream: null,
        '+serviceContext': 'com.liferay.portal.service.ServiceContext',
        'serviceContext.companyId': userGroup.companyId,
        'serviceContext.scopeGroupId': userGroup.groupId,
        'serviceContext.addGuestPermissions': true,
        'serviceContext.addGroupPermissions': true
        // Issue #4
        /*serviceContext: {
          companyId: userGroup.groupId,
          scopeGroupId: userGroup.companyId,
          addGuestPermissions: true,
          addGroupPermissions: true
        }*/
      }
    })
    .then(function (result) {
      entry = result;

      entry.should.have.a.property('entryId');
      entry.should.have.a.property('title');
      entry.should.have.a.property('description');
      entry.should.have.a.property('content');

      entry.entryId.should.be.a.Number;
    });
  });

  it("should get that same entry", function () {
    return connection.invoke({
      "/blogsentry/get-entry": {
        entryId: entry.entryId
      }
    })
    .then(function (result) {
      result.should.eql(entry);
    });
  });

  it("should delete a fresh entry", function () {
    return connection.invoke({
      "/blogsentry/delete-entry": {
        entryId: entry.entryId
      }
    });
  });
});

if (config.TEST_COMPANION_PLUGIN) describe("Smoke tests (companion)", function () {
  it("should reach the companion (1/2)", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/smoke-test": {
        input: true
      }
    })
    .then(function (output) {
      output.should.be.false;
    });
  });

  it("should reach the companion (2/2)", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/smoke-test": {
        input: false
      }
    })
    .then(function (output) {
      output.should.be.true;
    });
  });
});

if (config.TEST_COMPANION_PLUGIN) describe("Return types (companion)", function () {
  it("should support JSONObject", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/json-object-test": {
        property: 'answer',
        value: '42'
      }
    })
    .then(function (jsonObject) {
      jsonObject.should.have.a.property("answer");
      jsonObject.answer.should.be.eql("42");
    });
  });

  it("should support User", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/current-user": {}
    })
    .then(function (user) {
      user.should.have.a.property('userId');
    })
  });
});

if (config.TEST_COMPANION_PLUGIN) describe("Service Context (companion)", function () {
  it("should support ServiceContext parameters", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/service-context-smoke-test": {
        currentUrl: 'nonsense',
        scopeGroupId: userGroup.groupId,
        '+serviceContext': 'com.liferay.portal.service.ServiceContext',
        'serviceContext.scopeGroupId': userGroup.groupId,
        'serviceContext.currentURL': 'nonsense'
      }
    })
    .then(function (ok) {
      ok.should.be.true;
    });
  });
});

if (config.TEST_COMPANION_PLUGIN) describe("Parameter names (companion)", function () {
  it("should support manually mangled parameters", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/parameter-names-test": {
        somethingUrl: 'abc',
        somethingOvPs: 'def'
      }
    })
    .then(function (ok) {
      ok.should.be.true;
    });
  });

  it("should support not-mangled parameters", function () {
    return connection.invoke({
      "/connector-companion-portlet/tests/parameter-names-test": {
        somethingURL: 'abc',
        somethingOVPs: 'def'
      }
    })
    .then(function (ok) {
      ok.should.be.true;
    });
  });
});
<|endoftext|>


<commit_before>describe('Webmaker Events – Home', function() {
  var upcomingEvents = element.all(by.css('.listing-home li'));

  beforeEach(function() {
    browser.get('http://localhost:1981/#!/');
  });

  it('should feature some events', function() {
    expect(upcomingEvents.count()).toBeGreaterThan(0);
  });

  it('should have 10 or less featured events', function() {
    expect(upcomingEvents.count()).toBeLessThan(11);
  });

});

describe('Webmaker Events – Upcoming Events', function() {
  var searchBox = element(by.css('[test-id=search-box]'));
  var submitSearchButton = element(by.css('[test-id=submit-search-btn]'));
  var clearSearchButton = element(by.css('[test-id=clear-btn]'));
  var searchResultCount = element(by.binding('searchResultCount'));

  beforeEach(function() {
    browser.get('http://localhost:1981/#!/events');
  });

  it('should empty searchbox when clear button is clicked', function () {
    searchBox.sendKeys('test');
    submitSearchButton.click();
    clearSearchButton.click();
    expect(searchBox.getText()).toEqual('');
  });

});
<commit_msg>Removed shebang from the URLs<commit_after>describe('Webmaker Events – Home', function() {
  var upcomingEvents = element.all(by.css('.listing-home li'));

  beforeEach(function() {
    browser.get('http://localhost:1981/');
  });

  it('should feature some events', function() {
    expect(upcomingEvents.count()).toBeGreaterThan(0);
  });

  it('should have 10 or less featured events', function() {
    expect(upcomingEvents.count()).toBeLessThan(11);
  });

});

describe('Webmaker Events – Upcoming Events', function() {
  var searchBox = element(by.css('[test-id=search-box]'));
  var submitSearchButton = element(by.css('[test-id=submit-search-btn]'));
  var clearSearchButton = element(by.css('[test-id=clear-btn]'));
  var searchResultCount = element(by.binding('searchResultCount'));

  beforeEach(function() {
    browser.get('http://localhost:1981/events');
  });

  it('should empty searchbox when clear button is clicked', function () {
    searchBox.sendKeys('test');
    submitSearchButton.click();
    clearSearchButton.click();
    expect(searchBox.getText()).toEqual('');
  });

});
<|endoftext|>