<commit_before>const crypto = require('crypto')
const execa = require('execa')
const fs = require('fs')
const mkdirp = require('mkdirp')
const path = require('path')
const os = require('os')
const stencila = require('stencila')

const version = require('../../package').version
const HostHttpServer = require('./HostHttpServer')
const { GET, POST, PUT } = require('../util/requests')

const DocumentBundle = require('../bundles/DocumentBundle')
const NodeContext = require('../node-context/NodeContext')

// Look up for classes available under the
// `new` sheme
const TYPES = {
  'DocumentBundle': DocumentBundle,
  'NodeContext': NodeContext
}

/**
 * A `Host` allows you to create, get, run methods of, and delete instances of various types.
 * The types can be thought of a "services" provided by the host e.g. `NoteContext`, `FilesystemStorer`
 *
 * The API of a host is similar to that of a HTTP server. It's methods names
 * (e.g. `post`, `get`) are similar to HTTP methods (e.g. `POST`, `GET`) but
 * the sematics sometimes differ (e.g. a host's `put()` method is used to call an
 * instance method)
 *
 * A `Host` is not limited to beng served by HTTP and it's methods are exposed by both `HostHttpServer`
 * and `HostWebsocketServer`. Those other classes are responsible for tasks associated with
 * their communication protocol (e.g. serialising and deserialising objects).
 *
 *
 * This is a singleton class. There should only ever be one `Host`
 * in memory in each process (although, for purposes of testing, this is not enforced)
 */
class Host {

  constructor () {
    this._id = `node-${crypto.randomBytes(24).toString('hex')}`
    this._servers = {}
    this._started = null
    this._heartbeat = null
    this._instances = {}
    this._peers = []
  }

  /**
   * Get unique ID of this host
   */
  get id () {
    return this._id
  }

  /**
   * Get the current user's Stencila data directory
   */
  userDir () {
    switch(process.platform) {
      case 'darwin':
        return path.join(process.env.HOME, 'Library', 'Preferences', 'Stencila')
      case 'linux':
        return path.join(process.env.HOME, '.local', 'share', 'stencila')
      case 'windows':
        return path.join(process.env.APPDATA, 'Stencila')
      default:
        return path.join(process.env.HOME, 'stencila')
    }
  }

  /**
   * Get the current Stencila temporary directory
   */
  tempDir () {
    return path.join(os.tmpdir(), 'stencila')
  }

  /**
   * Get the environment of this host including the version of Node.js and versions
   * of installed packages (local and globals)
   *
   * @return {Object} The environment as a object
   */
  environ () {
    // TODO package names and versions
    // See for example https://github.com/stencila/r/blob/8575f43096b6472fcc039e513a5f82a274864241/R/host.R#L91
    return {
      version: process.version,
      platform: process.platform,
      arch: process.arch,
      packages: {}
    }
  }

  /**
   * Get a manifest for this host
   *
   * The manifest describes the host and it's capabilities. It is used
   * by peer hosts to determine which "types" this host provides and
   * which "instances" have already been instantiated.
   *
   * @return {Promise} Resolves to a manifest object
   */
  manifest () {
    let new_ = {}
    for (let name of Object.keys(TYPES)) {
      new_[name] = TYPES[name].spec
    }
    let manifest = {
      stencila: {
        package: 'node',
        version: version
      },
      run: [process.execPath, '-e', "require('stencila-node').run()"],
      schemes: {
        new: new_
      }
    }
    if (this._started) {
      manifest = Object.assign(manifest, {
        id: this.id,
        process: process.pid,
        urls: this.urls,
        instances: Object.keys(this._instances),
        peers: this._peers
      })
    }
    return manifest
  }

  /**
   * Install this Stencila Host on this machine.
   *
   * Installation of a host involves creating a file `node.json` inside of
   * the user's Stencila data (see `userDir()`) directory which describes
   * the capabilities of this host.
   */
  install () {
    let dir = path.join(this.userDir(), 'hosts')
    mkdirp(dir, error => {
      if (error) throw error
      fs.writeFile(path.join(dir, 'node.json'), JSON.stringify(this.manifest(false), null, '  '), error => {
        if (error) throw error
      })
    })
  }

  /**
   * Create a new instance of a type
   *
   * @param  {string} address - Type of instance
   * @param  {object} options - Options to be passed to type constructor
   * @return {Promise} - Resolves to the ID string of newly created instance
   */
  post (type, name, options) {
    this.heartbeat()
    return Promise.resolve().then(() => {
      let Class = TYPES[type]
      if (Class) {
        // Type present locally
        let address = `name://${name || Math.floor((1 + Math.random()) * 1e6).toString(16)}`
        this._instances[address] = new Class(options)
        return address
      } else {
        // Type not present locally, see if a peer has it
        return Promise.resolve().then(() => {
          for (let peer of this._peers) {
            let types = peer.schemes['new']
            for (let key of Object.keys(types)) {
              let type_ = types[key]
              if (type_.name === type) {
                if (peer.process) {
                  // Peer is active, so use it
                  return peer
                } else {
                  // Peer is inactive, so `spawn` it
                  return this.spawn(peer)
                }
              }
            }
          }
          throw new Error(`Unknown type: ${type}`)
        }).then(peer => {
          // Request the peer to create a new instance of type
          let url = peer.urls[0]
          return POST(url + '/' + type, Object.assign({name: name}, options)).then(address => {
            // Store the instance as a URL to be proxied to. In other methods (e.g. `put`),
            // string instances are recognised as remote instances and requests are proxied to them
            this._instances[address] = `${url}/${address}`
            return address
          })
        })
      }
    })
  }

  /**
   * Get an instance
   *
   * @param  {string} address - Address of instance
   * @return {Promise} - Resolves to the instance
   */
  get (address, proxy=true) {
    this.heartbeat()
    return Promise.resolve().then(() => {
      let instance
      if (address) {
        address = stencila.address.long(address)
        instance = this._instances[address]
      } else {
        instance = this
      }
      if (instance) {
        if (typeof instance !== 'string') {
          // Return local instance
          return instance
        }
        else {
          // Proxy request to peer
          return (proxy ? GET(instance) : instance)
        }
      } else {
        // Check if the address matches a registered type
        for (let name of Object.keys(TYPES)) {
          let Class = TYPES[name]
          if (typeof Class.match === 'function') {
            let match = Class.match(address)
            if (match) {
              let instance = new Class(match)
              this._instances[match] = instance
              return instance.import().then(() => {
                return instance
              })
            }
          }
        }
        throw new Error(`Unknown instance: ${address}`)
      }
    })
  }

  /**
   * Call a method of an instance
   *
   * @param  {string} id - ID of instance
   * @param {string} method - Name of instance method
   * @param {array} args - An array of method arguments
   * @return {Promise} Resolves to result of method call
   */
  put (address, method, args) {
    this.heartbeat()
    return this.get(address, false).then(instance => {
      if (typeof instance !== 'string') {
        let func = instance[method]
        if (func) {
          // Ensure arguments are an array
          if (args && !(args instanceof Array)) {
            if (args instanceof Object) {
              args = Object.keys(args).map(key => args[key])
            } else {
              args = [args]
            }
          }
          // Return method call result
          return instance[method](...args)
        } else {
          throw new Error(`Unknown method: ${method}`)
        }
      }
      else {
        // Proxy request to peer
        return PUT(`${instance}!${method}`, args)
      }
    })
  }

  /**
   * Delete an instance
   *
   * @param  {string} id - ID of the instance
   * @return {Promise}
   */
  delete (id) {
    this.heartbeat()
    return new Promise((resolve, reject) => {
      let instance = this._instances[id]
      if (instance) {
        delete this._instances[id]
        resolve()
      } else {
        reject(new Error(`Unknown instance: ${id}`))
      }
    })
  }

  /**
   * Start serving this host
   *
   * Currently, HTTP is the only server available
   * for hosts. We plan to implement a `HostWebsocketServer` soon.
   *
   * @return {Promise}
   */
  start (address='127.0.0.1', port=2000) {
    return new Promise((resolve) => {
      if (!this._servers.http) {
        // Start HTTP server
        var server = new HostHttpServer(this, address, port)
        this._servers.http = server
        server.start().then(() => {

          // Record start times
          this._started = new Date()
          this._heartbeat = new Date()

          // Register as a running host by creating a run file
          let file = path.join(this.tempDir(), 'hosts', this.id + '.json')
          mkdirp(path.dirname(file), error => {
            if (error) throw error
            fs.writeFile(file, JSON.stringify(this.manifest(), null, '  '), { mode: '600' }, error => {
              if (error) throw error
            })
          })
          console.log('Host has started at: ' + this.urls.join(', ')) // eslint-disable-line no-console

          // Discover other hosts
          this.discover()

          resolve()
        })
      } else {
        resolve()
      }
    })
  }

  /**
   * Update this host's heartbeat
   *
   * Can be called explictly by a peer (i.e. `PUT /!heartbeat`)
   * but also called by the `post`, `get`, `put` and `delete` methods
   * above.
   *
   * @return {Date} Date/time of last heartbeat
   */
  heartbeat () {
    this._heartbeat = new Date()
    return this._heartbeat
  }

  /**
   * Stop serving this host
   *
   * @return {Promise}
   */
  stop () {
    return new Promise((resolve) => {
      const type = 'http'
      let server = this._servers[type]
      if (server) {
        delete this._servers[type]
        server.stop().then(() => {
          // Deregister as a running host by removing run file
          let file = path.join(this.tempDir(), 'hosts', this.id + '.json')
          fs.unlink(file, () => {})
          console.log('Host has stopped.') // eslint-disable-line no-console
          resolve()
        })
      }
    })
  }

  /**
   * Start serving this host and wait for connections
   * indefinitely
   *
   * @return {Promise}
   */
  run (address='127.0.0.1', port=2000, timeout=Infinity, duration=Infinity) {
    const stop = () => {
      this.stop().then(() => {
        process.exit()
      })
    }

    // Setup timer to check timeout and duration every minute
    setInterval(()=>{
      const time = new Date()
      if ((time - this._heartbeat)/1000 > timeout || (time - this._started)/1000 >= duration) {
        stop();
      }
    }, 60 * 1000)

    // Handle interrupt
    if (process.platform === "win32") {
      var rl = require("readline").createInterface({
        input: process.stdin,
        output: process.stdout
      })
      rl.on("SIGINT", () => process.emit("SIGINT"))
    }
    process.on("SIGINT", stop)

    return this.start(address, port)
  }

  /**
   * Get a list of server names for this host
   *
   * Servers are identified by the protocol shorthand
   * e.g. `http` for `HostHttpServer`
   *
   * @return {array} Array of strings
   */
  get servers () {
    return Object.keys(this._servers)
  }

  /**
   * Get a list of URLs for this host
   *
   * @return {array} Array of strings
   */
  get urls () {
    return Object.values(this._servers).map(server => server.url)
  }

  /**
   * View this host in the browser
   *
   * Opens the default browser at the URL of this host
   */
  view () {
    // Difficult to test headlessly, so don't include in coverage
    /* istanbul ignore next */
    Promise.resolve(
      this.start()
        .then(() => {
          let url = this._servers.http.url
          if (os.platform() === 'linux') {
            execa(`2>/dev/null 1>&2 xdg-open "${url}"`)
          } else {
            execa(`open "${url}"`)
          }
        })
    )
  }

  /**
   * Get this host's peers
   */
  get peers () {
    return this._peers
  }

  /**
   * Discover peers
   *
   * Looks for peer hosts in the following locations (on Linux, equivalents on other OSs):
   *
   * - `/tmp/stencila/hosts` - hosts that are currently active (i.e. running)
   * - `~/.local/share/stencila/hosts` - hosts that are installed but inactive
   *
   * Set the `interval` parameter to trigger ongoing discovery.
   *
   * @param {number} interval - The interval (seconds) between discovery attempts
   */
  discover (interval=0) {
    let discoverDir = dir => {
      fs.access(dir, fs.constants.R_OK, error => {
        if (error) return
        fs.readdir(dir, (error, files) => {
          if (error) console.warn(error)
          for (let file of files) {
            let json
            try {
              json = fs.readFileSync(path.join(dir, file), { encoding: 'utf8' })
            } catch (readError) {
              console.warn(readError)
              return
            }
            let manifest
            try {
              manifest = JSON.parse(json)
            } catch (parseError) {
              console.warn(`Could not parse ${dir}/${file}: ${parseError}`)
              return
            }
            // If the manifest defines a `process` then check that process is actually running
            if (manifest.process) {
              try {
                process.kill(manifest.process, 0)
              } catch (exception) {
                continue
              }
            }
            // Insert if the peer is not already in this._peers
            let insert = true
            if (manifest.id) {
              for (let peer of this._peers) {
                if (peer.id === manifest.id) {
                  insert = false
                  break
                }
              }
            }
            if (insert) this._peers.push(manifest)
          }
        })
      })
    }

    // Discover active hosts first
    discoverDir(path.join(this.tempDir(), 'hosts'))
    discoverDir(path.join(this.userDir(), 'hosts'))

    if (interval) setTimeout(() => this.discover(interval), interval*1000)
  }

  /**
   * Spawn a peer from an inactive host manifest
   *
   * @param  {[type]} peer [description]
   * @return {[type]}      [description]
   */
  spawn (host) {
    return new Promise((resolve) => {
      let run = host.run
      let child = execa(run[0], run.slice(1))
      child.stdout.on('data', data => {
        let manifest = JSON.parse(data.toString())
        // Place at start of peers list
        this._peers.unshift(manifest)
        resolve(manifest)
      })
    })
  }

}

module.exports = Host
<commit_msg>Use 'Application Support' on Mac OS<commit_after>const crypto = require('crypto')
const execa = require('execa')
const fs = require('fs')
const mkdirp = require('mkdirp')
const path = require('path')
const os = require('os')
const stencila = require('stencila')

const version = require('../../package').version
const HostHttpServer = require('./HostHttpServer')
const { GET, POST, PUT } = require('../util/requests')

const DocumentBundle = require('../bundles/DocumentBundle')
const NodeContext = require('../node-context/NodeContext')

// Look up for classes available under the
// `new` sheme
const TYPES = {
  'DocumentBundle': DocumentBundle,
  'NodeContext': NodeContext
}

/**
 * A `Host` allows you to create, get, run methods of, and delete instances of various types.
 * The types can be thought of a "services" provided by the host e.g. `NoteContext`, `FilesystemStorer`
 *
 * The API of a host is similar to that of a HTTP server. It's methods names
 * (e.g. `post`, `get`) are similar to HTTP methods (e.g. `POST`, `GET`) but
 * the sematics sometimes differ (e.g. a host's `put()` method is used to call an
 * instance method)
 *
 * A `Host` is not limited to beng served by HTTP and it's methods are exposed by both `HostHttpServer`
 * and `HostWebsocketServer`. Those other classes are responsible for tasks associated with
 * their communication protocol (e.g. serialising and deserialising objects).
 *
 *
 * This is a singleton class. There should only ever be one `Host`
 * in memory in each process (although, for purposes of testing, this is not enforced)
 */
class Host {

  constructor () {
    this._id = `node-${crypto.randomBytes(24).toString('hex')}`
    this._servers = {}
    this._started = null
    this._heartbeat = null
    this._instances = {}
    this._peers = []
  }

  /**
   * Get unique ID of this host
   */
  get id () {
    return this._id
  }

  /**
   * Get the current user's Stencila data directory
   */
  userDir () {
    switch(process.platform) {
      case 'darwin':
        return path.join(process.env.HOME, 'Library', 'Application Support', 'Stencila')
      case 'linux':
        return path.join(process.env.HOME, '.local', 'share', 'stencila')
      case 'windows':
        return path.join(process.env.APPDATA, 'Stencila')
      default:
        return path.join(process.env.HOME, 'stencila')
    }
  }

  /**
   * Get the current Stencila temporary directory
   */
  tempDir () {
    return path.join(os.tmpdir(), 'stencila')
  }

  /**
   * Get the environment of this host including the version of Node.js and versions
   * of installed packages (local and globals)
   *
   * @return {Object} The environment as a object
   */
  environ () {
    // TODO package names and versions
    // See for example https://github.com/stencila/r/blob/8575f43096b6472fcc039e513a5f82a274864241/R/host.R#L91
    return {
      version: process.version,
      platform: process.platform,
      arch: process.arch,
      packages: {}
    }
  }

  /**
   * Get a manifest for this host
   *
   * The manifest describes the host and it's capabilities. It is used
   * by peer hosts to determine which "types" this host provides and
   * which "instances" have already been instantiated.
   *
   * @return {Promise} Resolves to a manifest object
   */
  manifest () {
    let new_ = {}
    for (let name of Object.keys(TYPES)) {
      new_[name] = TYPES[name].spec
    }
    let manifest = {
      stencila: {
        package: 'node',
        version: version
      },
      run: [process.execPath, '-e', "require('stencila-node').run()"],
      schemes: {
        new: new_
      }
    }
    if (this._started) {
      manifest = Object.assign(manifest, {
        id: this.id,
        process: process.pid,
        urls: this.urls,
        instances: Object.keys(this._instances),
        peers: this._peers
      })
    }
    return manifest
  }

  /**
   * Install this Stencila Host on this machine.
   *
   * Installation of a host involves creating a file `node.json` inside of
   * the user's Stencila data (see `userDir()`) directory which describes
   * the capabilities of this host.
   */
  install () {
    let dir = path.join(this.userDir(), 'hosts')
    mkdirp(dir, error => {
      if (error) throw error
      fs.writeFile(path.join(dir, 'node.json'), JSON.stringify(this.manifest(false), null, '  '), error => {
        if (error) throw error
      })
    })
  }

  /**
   * Create a new instance of a type
   *
   * @param  {string} address - Type of instance
   * @param  {object} options - Options to be passed to type constructor
   * @return {Promise} - Resolves to the ID string of newly created instance
   */
  post (type, name, options) {
    this.heartbeat()
    return Promise.resolve().then(() => {
      let Class = TYPES[type]
      if (Class) {
        // Type present locally
        let address = `name://${name || Math.floor((1 + Math.random()) * 1e6).toString(16)}`
        this._instances[address] = new Class(options)
        return address
      } else {
        // Type not present locally, see if a peer has it
        return Promise.resolve().then(() => {
          for (let peer of this._peers) {
            let types = peer.schemes['new']
            for (let key of Object.keys(types)) {
              let type_ = types[key]
              if (type_.name === type) {
                if (peer.process) {
                  // Peer is active, so use it
                  return peer
                } else {
                  // Peer is inactive, so `spawn` it
                  return this.spawn(peer)
                }
              }
            }
          }
          throw new Error(`Unknown type: ${type}`)
        }).then(peer => {
          // Request the peer to create a new instance of type
          let url = peer.urls[0]
          return POST(url + '/' + type, Object.assign({name: name}, options)).then(address => {
            // Store the instance as a URL to be proxied to. In other methods (e.g. `put`),
            // string instances are recognised as remote instances and requests are proxied to them
            this._instances[address] = `${url}/${address}`
            return address
          })
        })
      }
    })
  }

  /**
   * Get an instance
   *
   * @param  {string} address - Address of instance
   * @return {Promise} - Resolves to the instance
   */
  get (address, proxy=true) {
    this.heartbeat()
    return Promise.resolve().then(() => {
      let instance
      if (address) {
        address = stencila.address.long(address)
        instance = this._instances[address]
      } else {
        instance = this
      }
      if (instance) {
        if (typeof instance !== 'string') {
          // Return local instance
          return instance
        }
        else {
          // Proxy request to peer
          return (proxy ? GET(instance) : instance)
        }
      } else {
        // Check if the address matches a registered type
        for (let name of Object.keys(TYPES)) {
          let Class = TYPES[name]
          if (typeof Class.match === 'function') {
            let match = Class.match(address)
            if (match) {
              let instance = new Class(match)
              this._instances[match] = instance
              return instance.import().then(() => {
                return instance
              })
            }
          }
        }
        throw new Error(`Unknown instance: ${address}`)
      }
    })
  }

  /**
   * Call a method of an instance
   *
   * @param  {string} id - ID of instance
   * @param {string} method - Name of instance method
   * @param {array} args - An array of method arguments
   * @return {Promise} Resolves to result of method call
   */
  put (address, method, args) {
    this.heartbeat()
    return this.get(address, false).then(instance => {
      if (typeof instance !== 'string') {
        let func = instance[method]
        if (func) {
          // Ensure arguments are an array
          if (args && !(args instanceof Array)) {
            if (args instanceof Object) {
              args = Object.keys(args).map(key => args[key])
            } else {
              args = [args]
            }
          }
          // Return method call result
          return instance[method](...args)
        } else {
          throw new Error(`Unknown method: ${method}`)
        }
      }
      else {
        // Proxy request to peer
        return PUT(`${instance}!${method}`, args)
      }
    })
  }

  /**
   * Delete an instance
   *
   * @param  {string} id - ID of the instance
   * @return {Promise}
   */
  delete (id) {
    this.heartbeat()
    return new Promise((resolve, reject) => {
      let instance = this._instances[id]
      if (instance) {
        delete this._instances[id]
        resolve()
      } else {
        reject(new Error(`Unknown instance: ${id}`))
      }
    })
  }

  /**
   * Start serving this host
   *
   * Currently, HTTP is the only server available
   * for hosts. We plan to implement a `HostWebsocketServer` soon.
   *
   * @return {Promise}
   */
  start (address='127.0.0.1', port=2000) {
    return new Promise((resolve) => {
      if (!this._servers.http) {
        // Start HTTP server
        var server = new HostHttpServer(this, address, port)
        this._servers.http = server
        server.start().then(() => {

          // Record start times
          this._started = new Date()
          this._heartbeat = new Date()

          // Register as a running host by creating a run file
          let file = path.join(this.tempDir(), 'hosts', this.id + '.json')
          mkdirp(path.dirname(file), error => {
            if (error) throw error
            fs.writeFile(file, JSON.stringify(this.manifest(), null, '  '), { mode: '600' }, error => {
              if (error) throw error
            })
          })
          console.log('Host has started at: ' + this.urls.join(', ')) // eslint-disable-line no-console

          // Discover other hosts
          this.discover()

          resolve()
        })
      } else {
        resolve()
      }
    })
  }

  /**
   * Update this host's heartbeat
   *
   * Can be called explictly by a peer (i.e. `PUT /!heartbeat`)
   * but also called by the `post`, `get`, `put` and `delete` methods
   * above.
   *
   * @return {Date} Date/time of last heartbeat
   */
  heartbeat () {
    this._heartbeat = new Date()
    return this._heartbeat
  }

  /**
   * Stop serving this host
   *
   * @return {Promise}
   */
  stop () {
    return new Promise((resolve) => {
      const type = 'http'
      let server = this._servers[type]
      if (server) {
        delete this._servers[type]
        server.stop().then(() => {
          // Deregister as a running host by removing run file
          let file = path.join(this.tempDir(), 'hosts', this.id + '.json')
          fs.unlink(file, () => {})
          console.log('Host has stopped.') // eslint-disable-line no-console
          resolve()
        })
      }
    })
  }

  /**
   * Start serving this host and wait for connections
   * indefinitely
   *
   * @return {Promise}
   */
  run (address='127.0.0.1', port=2000, timeout=Infinity, duration=Infinity) {
    const stop = () => {
      this.stop().then(() => {
        process.exit()
      })
    }

    // Setup timer to check timeout and duration every minute
    setInterval(()=>{
      const time = new Date()
      if ((time - this._heartbeat)/1000 > timeout || (time - this._started)/1000 >= duration) {
        stop();
      }
    }, 60 * 1000)

    // Handle interrupt
    if (process.platform === "win32") {
      var rl = require("readline").createInterface({
        input: process.stdin,
        output: process.stdout
      })
      rl.on("SIGINT", () => process.emit("SIGINT"))
    }
    process.on("SIGINT", stop)

    return this.start(address, port)
  }

  /**
   * Get a list of server names for this host
   *
   * Servers are identified by the protocol shorthand
   * e.g. `http` for `HostHttpServer`
   *
   * @return {array} Array of strings
   */
  get servers () {
    return Object.keys(this._servers)
  }

  /**
   * Get a list of URLs for this host
   *
   * @return {array} Array of strings
   */
  get urls () {
    return Object.values(this._servers).map(server => server.url)
  }

  /**
   * View this host in the browser
   *
   * Opens the default browser at the URL of this host
   */
  view () {
    // Difficult to test headlessly, so don't include in coverage
    /* istanbul ignore next */
    Promise.resolve(
      this.start()
        .then(() => {
          let url = this._servers.http.url
          if (os.platform() === 'linux') {
            execa(`2>/dev/null 1>&2 xdg-open "${url}"`)
          } else {
            execa(`open "${url}"`)
          }
        })
    )
  }

  /**
   * Get this host's peers
   */
  get peers () {
    return this._peers
  }

  /**
   * Discover peers
   *
   * Looks for peer hosts in the following locations (on Linux, equivalents on other OSs):
   *
   * - `/tmp/stencila/hosts` - hosts that are currently active (i.e. running)
   * - `~/.local/share/stencila/hosts` - hosts that are installed but inactive
   *
   * Set the `interval` parameter to trigger ongoing discovery.
   *
   * @param {number} interval - The interval (seconds) between discovery attempts
   */
  discover (interval=0) {
    let discoverDir = dir => {
      fs.access(dir, fs.constants.R_OK, error => {
        if (error) return
        fs.readdir(dir, (error, files) => {
          if (error) console.warn(error)
          for (let file of files) {
            let json
            try {
              json = fs.readFileSync(path.join(dir, file), { encoding: 'utf8' })
            } catch (readError) {
              console.warn(readError)
              return
            }
            let manifest
            try {
              manifest = JSON.parse(json)
            } catch (parseError) {
              console.warn(`Could not parse ${dir}/${file}: ${parseError}`)
              return
            }
            // If the manifest defines a `process` then check that process is actually running
            if (manifest.process) {
              try {
                process.kill(manifest.process, 0)
              } catch (exception) {
                continue
              }
            }
            // Insert if the peer is not already in this._peers
            let insert = true
            if (manifest.id) {
              for (let peer of this._peers) {
                if (peer.id === manifest.id) {
                  insert = false
                  break
                }
              }
            }
            if (insert) this._peers.push(manifest)
          }
        })
      })
    }

    // Discover active hosts first
    discoverDir(path.join(this.tempDir(), 'hosts'))
    discoverDir(path.join(this.userDir(), 'hosts'))

    if (interval) setTimeout(() => this.discover(interval), interval*1000)
  }

  /**
   * Spawn a peer from an inactive host manifest
   *
   * @param  {[type]} peer [description]
   * @return {[type]}      [description]
   */
  spawn (host) {
    return new Promise((resolve) => {
      let run = host.run
      let child = execa(run[0], run.slice(1))
      child.stdout.on('data', data => {
        let manifest = JSON.parse(data.toString())
        // Place at start of peers list
        this._peers.unshift(manifest)
        resolve(manifest)
      })
    })
  }

}

module.exports = Host
<|endoftext|>


<commit_before>const http = require('http');
const request = require('request');
const bonjour = require('bonjour')();
const config = require('../config');

function HttpServer() {
  if (!(this instanceof HttpServer)) return new HttpServer();

  const port = 8080;

  const self = this;

  self.deviceStatus = {};

  self.httpRequestHandler = function httpRequestHandler(req, res) {
    if (req.url === '/') {
      res.setHeader('Content-Type', 'application/json');
      if (config.resin) {
        const resinSupervisorUrl = `${
          process.env.RESIN_SUPERVISOR_ADDRESS
        }/v1/device?apikey=${process.env.RESIN_SUPERVISOR_API_KEY}`;
        request.get(resinSupervisorUrl, (err, response, body) => {
          res.end(
            JSON.stringify(
              Object.assign(self.deviceStatus, {
                resinSupervisorStatus: JSON.parse(body),
              })
            )
          );
        });
      } else {
        res.end(JSON.stringify(self.deviceStatus));
      }
    } else {
      res.statusCode = 404;
      res.end();
    }
  };

  http.createServer(self.httpRequestHandler).listen(port);

  bonjour.publish({ name: 'BBQ-Pi', type: 'http', port });
}

HttpServer.prototype.setDeviceStatus = function setDeviceStatus(deviceStatus) {
  this.deviceStatus = deviceStatus;
};

module.exports = HttpServer;
<commit_msg>fix(http): remove buggy bonjour implementation<commit_after>const http = require('http');
const request = require('request');
const config = require('../config');

function HttpServer() {
  if (!(this instanceof HttpServer)) return new HttpServer();

  const port = 8080;

  const self = this;

  self.deviceStatus = {};

  self.httpRequestHandler = function httpRequestHandler(req, res) {
    if (req.url === '/') {
      res.setHeader('Content-Type', 'application/json');
      if (config.resin) {
        const resinSupervisorUrl = `${
          process.env.RESIN_SUPERVISOR_ADDRESS
        }/v1/device?apikey=${process.env.RESIN_SUPERVISOR_API_KEY}`;
        request.get(resinSupervisorUrl, (err, response, body) => {
          res.end(
            JSON.stringify(
              Object.assign(self.deviceStatus, {
                resinSupervisorStatus: JSON.parse(body),
              })
            )
          );
        });
      } else {
        res.end(JSON.stringify(self.deviceStatus));
      }
    } else {
      res.statusCode = 404;
      res.end();
    }
  };

  http.createServer(self.httpRequestHandler).listen(port);
}

HttpServer.prototype.setDeviceStatus = function setDeviceStatus(deviceStatus) {
  this.deviceStatus = deviceStatus;
};

module.exports = HttpServer;
<|endoftext|>


<commit_before>const http = require('http');
const bonjour = require('bonjour')();

function HttpServer() {
  if (!(this instanceof HttpServer)) return new HttpServer();

  const port = 80;

  this.deviceStatus = {};

  http.createServer((request, response) => {
    response.setHeader('Content-Type', 'application/json');
    response.end(this.deviceStatus);
  }).listen(port);

  bonjour.publish({ name: 'BBQ-Pi', type: 'http', port: 80 });
}

HttpServer.prototype.setDeviceStatus = function setDeviceStatus(deviceStatus) {
  this.deviceStatus = deviceStatus;
};

module.exports = HttpServer;
<commit_msg>fix(http): Fix format of http response<commit_after>const http = require('http');
const bonjour = require('bonjour')();

function HttpServer() {
  if (!(this instanceof HttpServer)) return new HttpServer();

  const port = 80;

  this.deviceStatus = {};

  http.createServer((request, response) => {
    response.setHeader('Content-Type', 'application/json');
    response.end(JSON.stringify(this.deviceStatus));
  }).listen(port);

  bonjour.publish({ name: 'BBQ-Pi', type: 'http', port: 80 });
}

HttpServer.prototype.setDeviceStatus = function setDeviceStatus(deviceStatus) {
  this.deviceStatus = deviceStatus;
};

module.exports = HttpServer;
<|endoftext|>


<commit_before>/*jslint indent: 2, nomen: true, maxlen: 100, white: true  plusplus: true */
/*global beforeEach, afterEach */
/*global describe, it, expect, jasmine*/
/*global runs, waitsFor, spyOn, waits */
/*global document, window, helper */
/*global $, _, d3*/
/*global GraphViewerPreview, mocks, uiMatchers*/

////////////////////////////////////////////////////////////////////////////////
/// @brief Graph functionality
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2010-2012 triagens GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Michael Hackstein
/// @author Copyright 2011-2013, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////

(function () {
  "use strict";

  describe('Graph Viewer Preview', function () {

    var width, height, cont;

    beforeEach(function() {
      width = 300;
      height = 300;
      cont = document.createElement("div");
      document.body.appendChild(cont);
      cont.style.height = height + "px";
      cont.style.width =  width + "px";
      uiMatchers.define(this);
    });

    afterEach(function() {
      document.body.removeChild(cont);
    });

    describe('setup process', function() {

      it('should append an svg to the container', function() {
        var ui = new GraphViewerPreview(cont),
          gsvg = $("#graphViewerSVG", $(cont));
        expect(gsvg.length).toEqual(1);
        expect(gsvg.get(0)).toBeTag("svg");
      });

      it('should create a graphViewer with container dimensions and preview adapter', function() {
        spyOn(window, "GraphViewer").andCallThrough();
        var ui = new GraphViewerPreview(cont);
        expect(window.GraphViewer).wasCalledWith(
          jasmine.any(Object),
          cont.offsetWidth,
          cont.offsetHeight,
          {
            type: "preview",
            width: width,
            height: height
          },
          jasmine.any(Object)
        );
      });

      it('should automatically load the first node', function() {
        var mockObj = {
            loadInitialNode: function() {},
            explore: function() {}
          },
          startNode = "1",
          ui;

        spyOn(window, "PreviewAdapter").andCallFake(function() {
          return mockObj;
        });
        spyOn(mockObj, "loadInitialNode");
        ui = new GraphViewerPreview(cont, {});
        expect(mockObj.loadInitialNode).wasCalledWith(startNode, jasmine.any(Function));

      });

      it('should overwrite the nodeShaper label attribute', function() {
        var oldShapes = window.NodeShaper.shapes,
          spy = spyOn(window, "NodeShaper").andCallThrough(),
          config = {
            nodeShaper: {
              label: "Foxx"
            }
          },
          ui;
        spy.shapes = oldShapes;
        ui  = new GraphViewerPreview(cont, config);
        expect(window.NodeShaper).wasCalledWith(
          jasmine.any(Object),
          {
            label: "label",
            shape: jasmine.any(Object),
            color: jasmine.any(Object),
            distortion: "reset"
          },
          undefined
        );
      });

      it('should overwrite the source attribute if image should be displayed', function() {
        var oldShapes = window.NodeShaper.shapes,
          width = 32,
          height = 23,
          spy = spyOn(window, "NodeShaper").andCallThrough(),
          config = {
            nodeShaper: {
              shape: {
                type: oldShapes.IMAGE,
                width: width,
                height: height,
                source: "Foxx"
              }
            }
          },
          ui;
        spy.shapes = oldShapes;

        ui = new GraphViewerPreview(cont, config);
        expect(window.NodeShaper).wasCalledWith(
          jasmine.any(Object),
          {
            shape: {
              type: oldShapes.IMAGE,
              width: width,
              height: height,
              source: "image"
            },
            color: jasmine.any(Object),
            distortion: "reset"
          },
          undefined
        );
      });

      it('should overwrite the edgeShaper label attribute', function() {
        var oldShapes = window.EdgeShaper.shapes,
          spy = spyOn(window, "EdgeShaper").andCallThrough(),
          config = {
            edgeShaper: {
              label: "Foxx"
            }
          },
          ui;
        spy.shapes = oldShapes;
        ui  = new GraphViewerPreview(cont, config);
        expect(window.EdgeShaper).wasCalledWith(
          jasmine.any(Object),
          {
            label: "label",
            color: jasmine.any(Object)
          }
        );
      });

    });

    describe('testing mouse actions', function() {
      var disp;

      beforeEach(function() {
        var OrigDisp = window.EventDispatcher;
        spyOn(window, "EventDispatcher").andCallFake(function(ns, es, conf) {
          disp = new OrigDisp(ns, es, conf);
          spyOn(disp, "rebind");
          return disp;
        });
      });

      it('should be able to allow zoom', function() {
        spyOn(window, "ZoomManager");
        var config = {
            zoom: true
          },
          ui = new GraphViewerPreview(cont, config);
        expect(window.ZoomManager).wasCalled();
      });

      it('should not configure zoom if it is undefined', function() {
        spyOn(window, "ZoomManager");
        var ui = new GraphViewerPreview(cont);
        expect(window.ZoomManager).wasNotCalled();
      });

      it('should not configure zoom if it is forbidden', function() {
        spyOn(window, "ZoomManager");
        var config = {
            zoom: false
          },
          ui = new GraphViewerPreview(cont, config);
        expect(window.ZoomManager).wasNotCalled();
      });

      it('should be able to bind drag initially', function() {
        var OrigLayouter = window.ForceLayouter,
          layouter,
          actions = {
            drag: true
          },
          config = {
            actions: actions
          },
          ui;
        spyOn(window, "ForceLayouter").andCallFake(function(c) {
          layouter = new OrigLayouter(c);
          spyOn(layouter, "drag");
          return layouter;
        });

        ui = new GraphViewerPreview(cont, config);
        expect(disp.rebind).wasCalledWith("nodes", {drag: layouter.drag});
        expect(disp.rebind).wasCalledWith("edges", undefined);
        expect(disp.rebind).wasCalledWith("svg", undefined);
      });

      it('should be able to bind edit initially', function() {
        var actions = {
          edit: true
        },
        config = {
          actions: actions
        },
        ui = new GraphViewerPreview(cont, config);
        expect(disp.rebind).wasCalledWith("nodes", {click: jasmine.any(Function)});
        expect(disp.rebind).wasCalledWith("edges", {click: jasmine.any(Function)});
        expect(disp.rebind).wasCalledWith("svg", undefined);
      });

      it('should be able to bind create initially', function() {
        var actions = {
          create: true
        },
        config = {
          actions: actions
        },
        ui = new GraphViewerPreview(cont, config);
        expect(disp.rebind).wasCalledWith("nodes", {
          mousedown: jasmine.any(Function),
          mouseup: jasmine.any(Function)
        });
        expect(disp.rebind).wasCalledWith("edges", undefined);
        expect(disp.rebind).wasCalledWith("svg", {
          click: jasmine.any(Function),
          mouseup: jasmine.any(Function)
        });
      });

      it('should be able to bind remove initially', function() {
        var actions = {
          remove: true
        },
        config = {
          actions: actions
        },
        ui = new GraphViewerPreview(cont, config);
        expect(disp.rebind).wasCalledWith("nodes", {
          click: jasmine.any(Function)
        });
        expect(disp.rebind).wasCalledWith("edges", {
          click: jasmine.any(Function)
        });
        expect(disp.rebind).wasCalledWith("svg", undefined);
      });

      it('should be able to bind expand initially', function() {
        var actions = {
          expand: true
        },
        config = {
          actions: actions
        },
        ui = new GraphViewerPreview(cont, config);
        expect(disp.rebind).wasCalledWith("nodes", {
          click: jasmine.any(Function)
        });
        expect(disp.rebind).wasCalledWith("edges", undefined);
        expect(disp.rebind).wasCalledWith("svg", undefined);
      });

    });

    describe('testing toolbox', function() {
      var toolboxSelector = "#toolbox";

      it('should append the toolbox if any tool is added', function() {
        var toolboxConf = {
            expand: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);
        expect($(toolboxSelector).length).toEqual(1);
      });

      it('should reduce the size of the svg', function() {
        var toolboxConf = {
            expand: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui;

        spyOn(window, "GraphViewer").andCallThrough();
        ui = new GraphViewerPreview(cont, config);
        expect(window.GraphViewer).wasCalledWith(
          jasmine.any(Object),
          cont.offsetWidth - 43,
          cont.offsetHeight,
          {
          	type: "preview",
            width: width - 43,
            height: height
          },
          jasmine.any(Object)
        );
      });

      /*
      it('should create the additional mouse-icon box', function() {
        var toolboxConf = {
            expand: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);
        expect(pointerBox.length).toEqual(1);
        expect(pointerBox[0]).toBeTag("div");
      });
      */
      /*
      it('should position the mouse-icon box next to the mouse pointer', function() {
        var toolboxConf = {
            expand: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config),
          x = 40,
          y = 50,
          pointerBox = $("#mousepointer");

        helper.simulateMouseMoveEvent("graphViewerSVG", x, y);
        expect(pointerBox.offset().left).toEqual(x + 7);
        expect(pointerBox.offset().top).toEqual(y + 12);

        x = 66;
        y = 33;

        helper.simulateMouseMoveEvent("graphViewerSVG", x, y);
        expect(pointerBox.offset().left).toEqual(x + 7);
        expect(pointerBox.offset().top).toEqual(y + 12);
      });
      */
      it('should not add a toolbox if no buttons are defined', function() {
        var toolboxConf = {},
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);
        expect($(toolboxSelector).length).toEqual(0);
      });

      it('should be able to add the expand button', function() {
        var toolboxConf = {
            expand: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_expand").length).toEqual(1);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

      it('should not add a toolbox if all buttons are defined false', function() {
        var toolboxConf = {
          expand: false
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);
      });

      it('should be able to add the expand button', function() {
        var toolboxConf = {
            expand: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_expand").length).toEqual(1);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

      it('should be able to add the drag button', function() {
        var toolboxConf = {
            drag: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_drag").length).toEqual(1);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

      it('should be able to add the create buttons', function() {
        var toolboxConf = {
            create: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_new_node").length).toEqual(1);
        expect($(toolboxSelector + " #control_event_connect").length).toEqual(1);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

      it('should be able to add the edit button', function() {
        var toolboxConf = {
            edit: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_edit").length).toEqual(1);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

      it('should be able to add the remove button', function() {
        var toolboxConf = {
            remove: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_delete").length).toEqual(1);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

      it('should not add buttons configured as false', function() {
        var toolboxConf = {
            expand: false,
            drag: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_expand").length).toEqual(0);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

    });

  });

}());
<commit_msg>Fixed tests for graph previewer<commit_after>/*jslint indent: 2, nomen: true, maxlen: 100, white: true  plusplus: true */
/*global beforeEach, afterEach */
/*global describe, it, expect, jasmine*/
/*global runs, waitsFor, spyOn, waits */
/*global document, window, helper */
/*global $, _, d3*/
/*global GraphViewerPreview, mocks, uiMatchers*/

////////////////////////////////////////////////////////////////////////////////
/// @brief Graph functionality
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2010-2012 triagens GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Michael Hackstein
/// @author Copyright 2011-2013, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////

(function () {
  "use strict";

  describe('Graph Viewer Preview', function () {

    var width, height, cont;

    beforeEach(function() {
      width = 300;
      height = 300;
      cont = document.createElement("div");
      document.body.appendChild(cont);
      cont.style.height = height + "px";
      cont.style.width =  width + "px";
      uiMatchers.define(this);
    });

    afterEach(function() {
      document.body.removeChild(cont);
    });

    describe('setup process', function() {

      it('should append an svg to the container', function() {
        var ui = new GraphViewerPreview(cont),
          gsvg = $("#graphViewerSVG", $(cont));
        expect(gsvg.length).toEqual(1);
        expect(gsvg.get(0)).toBeTag("svg");
      });

      it('should create a graphViewer with container dimensions and preview adapter', function() {
        spyOn(window, "GraphViewer").andCallThrough();
        var ui = new GraphViewerPreview(cont);
        expect(window.GraphViewer).wasCalledWith(
          jasmine.any(Object),
          cont.offsetWidth,
          cont.offsetHeight,
          {
            type: "preview",
            width: width,
            height: height
          },
          jasmine.any(Object)
        );
      });

      it('should automatically load the first node', function() {
        var mockObj = {
            loadInitialNode: function() {},
            explore: function() {}
          },
          startNode = "1",
          ui;

        spyOn(window, "PreviewAdapter").andCallFake(function() {
          return mockObj;
        });
        spyOn(mockObj, "loadInitialNode");
        ui = new GraphViewerPreview(cont, {});
        expect(mockObj.loadInitialNode).wasCalledWith(startNode, jasmine.any(Function));

      });

      it('should overwrite the nodeShaper label attribute', function() {
        var oldShapes = window.NodeShaper.shapes,
          spy = spyOn(window, "NodeShaper").andCallThrough(),
          config = {
            nodeShaper: {
              label: "Foxx"
            }
          },
          ui;
        spy.shapes = oldShapes;
        ui  = new GraphViewerPreview(cont, config);
        expect(window.NodeShaper).wasCalledWith(
          jasmine.any(Object),
          {
            label: "label",
            shape: jasmine.any(Object),
            color: jasmine.any(Object),
            distortion: "reset"
          },
          undefined
        );
      });

      it('should overwrite the source attribute if image should be displayed', function() {
        var oldShapes = window.NodeShaper.shapes,
          width = 32,
          height = 23,
          spy = spyOn(window, "NodeShaper").andCallThrough(),
          config = {
            nodeShaper: {
              shape: {
                type: oldShapes.IMAGE,
                width: width,
                height: height,
                source: "Foxx"
              }
            }
          },
          ui;
        spy.shapes = oldShapes;

        ui = new GraphViewerPreview(cont, config);
        expect(window.NodeShaper).wasCalledWith(
          jasmine.any(Object),
          {
            shape: {
              type: oldShapes.IMAGE,
              width: width,
              height: height,
              source: "image"
            },
            color: jasmine.any(Object),
            distortion: "reset"
          },
          undefined
        );
      });

      it('should overwrite the edgeShaper label attribute', function() {
        var oldShapes = window.EdgeShaper.shapes,
          spy = spyOn(window, "EdgeShaper").andCallThrough(),
          config = {
            edgeShaper: {
              label: "Foxx"
            }
          },
          ui;
        spy.shapes = oldShapes;
        ui  = new GraphViewerPreview(cont, config);
        expect(window.EdgeShaper).wasCalledWith(
          jasmine.any(Object),
          {
            label: "label",
            color: jasmine.any(Object),
            shape: jasmine.any(Object)
          }
        );
      });

    });

    describe('testing mouse actions', function() {
      var disp;

      beforeEach(function() {
        var OrigDisp = window.EventDispatcher;
        spyOn(window, "EventDispatcher").andCallFake(function(ns, es, conf) {
          disp = new OrigDisp(ns, es, conf);
          spyOn(disp, "rebind");
          return disp;
        });
      });

      it('should be able to allow zoom', function() {
        spyOn(window, "ZoomManager");
        var config = {
            zoom: true
          },
          ui = new GraphViewerPreview(cont, config);
        expect(window.ZoomManager).wasCalled();
      });

      it('should not configure zoom if it is undefined', function() {
        spyOn(window, "ZoomManager");
        var ui = new GraphViewerPreview(cont);
        expect(window.ZoomManager).wasNotCalled();
      });

      it('should not configure zoom if it is forbidden', function() {
        spyOn(window, "ZoomManager");
        var config = {
            zoom: false
          },
          ui = new GraphViewerPreview(cont, config);
        expect(window.ZoomManager).wasNotCalled();
      });

      it('should be able to bind drag initially', function() {
        var OrigLayouter = window.ForceLayouter,
          layouter,
          actions = {
            drag: true
          },
          config = {
            actions: actions
          },
          ui;
        spyOn(window, "ForceLayouter").andCallFake(function(c) {
          layouter = new OrigLayouter(c);
          spyOn(layouter, "drag");
          return layouter;
        });

        ui = new GraphViewerPreview(cont, config);
        expect(disp.rebind).wasCalledWith("nodes", {drag: layouter.drag});
        expect(disp.rebind).wasCalledWith("edges", undefined);
        expect(disp.rebind).wasCalledWith("svg", undefined);
      });

      it('should be able to bind edit initially', function() {
        var actions = {
          edit: true
        },
        config = {
          actions: actions
        },
        ui = new GraphViewerPreview(cont, config);
        expect(disp.rebind).wasCalledWith("nodes", {click: jasmine.any(Function)});
        expect(disp.rebind).wasCalledWith("edges", {click: jasmine.any(Function)});
        expect(disp.rebind).wasCalledWith("svg", undefined);
      });

      it('should be able to bind create initially', function() {
        var actions = {
          create: true
        },
        config = {
          actions: actions
        },
        ui = new GraphViewerPreview(cont, config);
        expect(disp.rebind).wasCalledWith("nodes", {
          mousedown: jasmine.any(Function),
          mouseup: jasmine.any(Function)
        });
        expect(disp.rebind).wasCalledWith("edges", undefined);
        expect(disp.rebind).wasCalledWith("svg", {
          click: jasmine.any(Function),
          mouseup: jasmine.any(Function)
        });
      });

      it('should be able to bind remove initially', function() {
        var actions = {
          remove: true
        },
        config = {
          actions: actions
        },
        ui = new GraphViewerPreview(cont, config);
        expect(disp.rebind).wasCalledWith("nodes", {
          click: jasmine.any(Function)
        });
        expect(disp.rebind).wasCalledWith("edges", {
          click: jasmine.any(Function)
        });
        expect(disp.rebind).wasCalledWith("svg", undefined);
      });

      it('should be able to bind expand initially', function() {
        var actions = {
          expand: true
        },
        config = {
          actions: actions
        },
        ui = new GraphViewerPreview(cont, config);
        expect(disp.rebind).wasCalledWith("nodes", {
          click: jasmine.any(Function)
        });
        expect(disp.rebind).wasCalledWith("edges", undefined);
        expect(disp.rebind).wasCalledWith("svg", undefined);
      });

    });

    describe('testing toolbox', function() {
      var toolboxSelector = "#toolbox";

      it('should append the toolbox if any tool is added', function() {
        var toolboxConf = {
            expand: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);
        expect($(toolboxSelector).length).toEqual(1);
      });

      it('should reduce the size of the svg', function() {
        var toolboxConf = {
            expand: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui;

        spyOn(window, "GraphViewer").andCallThrough();
        ui = new GraphViewerPreview(cont, config);
        expect(window.GraphViewer).wasCalledWith(
          jasmine.any(Object),
          cont.offsetWidth - 43,
          cont.offsetHeight,
          {
          	type: "preview",
            width: width - 43,
            height: height
          },
          jasmine.any(Object)
        );
      });

      /*
      it('should create the additional mouse-icon box', function() {
        var toolboxConf = {
            expand: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);
        expect(pointerBox.length).toEqual(1);
        expect(pointerBox[0]).toBeTag("div");
      });
      */
      /*
      it('should position the mouse-icon box next to the mouse pointer', function() {
        var toolboxConf = {
            expand: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config),
          x = 40,
          y = 50,
          pointerBox = $("#mousepointer");

        helper.simulateMouseMoveEvent("graphViewerSVG", x, y);
        expect(pointerBox.offset().left).toEqual(x + 7);
        expect(pointerBox.offset().top).toEqual(y + 12);

        x = 66;
        y = 33;

        helper.simulateMouseMoveEvent("graphViewerSVG", x, y);
        expect(pointerBox.offset().left).toEqual(x + 7);
        expect(pointerBox.offset().top).toEqual(y + 12);
      });
      */
      it('should not add a toolbox if no buttons are defined', function() {
        var toolboxConf = {},
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);
        expect($(toolboxSelector).length).toEqual(0);
      });

      it('should be able to add the expand button', function() {
        var toolboxConf = {
            expand: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_expand").length).toEqual(1);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

      it('should not add a toolbox if all buttons are defined false', function() {
        var toolboxConf = {
          expand: false
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);
      });

      it('should be able to add the expand button', function() {
        var toolboxConf = {
            expand: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_expand").length).toEqual(1);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

      it('should be able to add the drag button', function() {
        var toolboxConf = {
            drag: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_drag").length).toEqual(1);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

      it('should be able to add the create buttons', function() {
        var toolboxConf = {
            create: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_new_node").length).toEqual(1);
        expect($(toolboxSelector + " #control_event_connect").length).toEqual(1);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

      it('should be able to add the edit button', function() {
        var toolboxConf = {
            edit: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_edit").length).toEqual(1);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

      it('should be able to add the remove button', function() {
        var toolboxConf = {
            remove: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_delete").length).toEqual(1);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

      it('should not add buttons configured as false', function() {
        var toolboxConf = {
            expand: false,
            drag: true
          },
          config = {
            toolbox: toolboxConf
          },
          ui = new GraphViewerPreview(cont, config);

        expect($(toolboxSelector + " #control_event_expand").length).toEqual(0);
        expect($(toolboxSelector)[0]).toConformToToolboxLayout();
      });

    });

  });

}());
<|endoftext|>


<commit_before>describe("ember-oauth2", function() {

  window.App = Ember.Application.create({
    NAME: 'test-app'
  });

  var authorizeUri;
  var callbackUri;
  var savedState;

  var providerId, authBaseUri, callbackUriError, redirectUri, clientId, scope, state;

  var OAuth2;

  beforeEach(function() {
    providerId = 'test_auth';
    authBaseUri = 'https://foobar.dev/oauth/authorize';
    redirectUri = 'https://qux.dev/oauth/callback';
    clientId = '12345';
    scope = 'public';
    state = '12345';

    window.EmberENV = {};
    window.ENV = window.ENV || {};
    window.ENV['ember-oauth2'] = {
      test_auth: {
        clientId: clientId,
        authBaseUri: authBaseUri,
        redirectUri: redirectUri,
        scope: scope,
        state: state
      },
      test_auth_code: {
        clientId: clientId,
        authBaseUri: authBaseUri,
        redirectUri: redirectUri,
        responseType: 'code',
        scope: scope,
        state: state
      },
      test_auth_config: {
        clientId: clientId,
        authBaseUri: authBaseUri,
        redirectUri: redirectUri,
        scope: scope,
        state: state,
        statePrefix: 'foo',
        tokenPrefix: 'bar'
      }
    };

    OAuth2 = require('ember-oauth2')['default'];

    // setup test_auth
    App.oauth = OAuth2.create({providerId: providerId});

    // setup test_auth_config
    App.oauth_auth_config = OAuth2.create({providerId: 'test_auth_config'});

    // setup test_auth_code
    App.oauth_auth_code = OAuth2.create({providerId: 'test_auth_code'});

    callbackUri = redirectUri;
    callbackUri += '#access_token=' + ('12345abc') +
                '&token_type=' + 'Bearer' +
                '&expires_in=' + '3600' +
                '&state=' + state;

    callbackUriError = redirectUri + '?state=foobarqux';

    savedState = {
      provider_id: providerId,
      response_type: 'token',
      state: state,
      client_id: clientId,
      scope: scope
    };
  });

  afterEach(function() {
    authorizeUri = null;
  });

  var once = function(fn) {
    var returnValue, called = false;
    return function () {
      if (!called) {
          called = true;
          returnValue = fn.apply(this, arguments);
      }
      return returnValue;
    };
  };

  /* config test */
  describe("ENV['ember-oauth2'] namespace should exist", function() {
    it("should create a ENV object", function() {
      expect(window.ENV).toBeDefined();
    });

    it("should create a ENV['ember-oauth2] ", function() {
      expect(window.ENV['ember-oauth2']).toBeDefined();
    });
  });

  describe("initialize", function() {
    it("should be initialized with the properties of provider", function() {
      expect(App.oauth.providerId).toEqual('test_auth');
      expect(App.oauth.clientId).toEqual(clientId);
      expect(App.oauth.authBaseUri).toEqual(authBaseUri);
      expect(App.oauth.redirectUri).toEqual(redirectUri);
      expect(App.oauth.scope).toEqual(scope);
      expect(App.oauth.state).toEqual(state);
      expect(App.oauth.responseType).toEqual('token');
      expect(App.oauth.statePrefix).toEqual('state');
      expect(App.oauth.tokenPrefix).toEqual('token');
      expect(App.oauth.config).toEqual(window.ENV['ember-oauth2']);
      expect(App.oauth.providerConfig).toEqual(window.ENV['ember-oauth2']['test_auth']);
    });

    it("should set a custom state prefix", function() {
      expect(App.oauth_auth_config.statePrefix).toEqual('foo');
    });

    it("should set a custom token prefix", function() {
      expect(App.oauth_auth_config.tokenPrefix).toEqual('bar');
    });

    it("set the responseType to 'code'", function() {
      expect(App.oauth_auth_code.responseType).toEqual('code');
    });
  });

  // config errors
  describe("Errors when configuration is incomplete", function() {
    it("throws an error when there is no configuration", function() {
      window.ENV = undefined;
      expect(function() {OAuth2.create();}).toThrow(new Error('Cannot find the ember-oauth2 config.'));
    });

    it("should require a providerId in the config", function() {
      expect(function() {OAuth2.create({providerId: 'noProviderIdWithName'});}).toThrow(new Error("Cannot find the providerId: 'noProviderIdWithName' in the config."));
    });

    it("should require a clientId", function() {
      window.ENV['ember-oauth2'].test_auth.clientId = null;
      App.oauth = OAuth2.create({providerId: providerId});
      expect(function() {App.oauth.authorize();}).toThrow(new Error("No client id given."));
    });

    it("should require a authorization base uri", function() {
      window.ENV['ember-oauth2'].test_auth.authBaseUri = null;
      App.oauth = OAuth2.create({providerId: providerId});
      expect(function() {App.oauth.authorize();}).toThrow(new Error("No auth base uri given."));
    });

    it("should require a callback uri", function() {
      window.ENV['ember-oauth2'].test_auth.redirectUri = null;
      App.oauth = OAuth2.create({providerId: providerId});
      expect(function() {App.oauth.authorize();}).toThrow(new Error("No redirect uri given."));
    });
  });

  // dialog window
  describe("Dialog window returns an Ember.RSVP.Promise", function() {
    var promise;
    var errorMessage;
    beforeEach(function() {
      errorMessage = 'error message.';
      App.oauth = OAuth2.create({providerId: providerId});
    });

    it("should resolve on success", function(done){
      promise = App.oauth.openWindow();
      promise.then(function(dialog) {
        expect(dialog).toBeDefined();
        dialog.close();
      }, function(error) {
        expect(error.message).not.toBeDefined();
      }).finally(done);
    });

    it("should error when dialog does not open", function(done) {
      var stub = sinon.stub(window, 'open').returns(false);
      promise = App.oauth.openWindow();
      promise.then(function(dialog) {
        expect(dialog).not.toBeDefined();
      }, function(error) {
        expect(error.message).toEqual('Opening dialog login window failed.');
      }).finally(done);
      stub.reset();
    });
  });

  // generate oauth2 url
  describe("Generate OAuth2 providers url", function() {
    describe("Implicit Grant authorize uri", function() {
      beforeEach(function() {
        authorizeUri = authBaseUri;
        authorizeUri += '?response_type=' + encodeURIComponent(App.oauth.responseType) +
                     '&redirect_uri=' + encodeURIComponent(redirectUri) +
                     '&client_id=' + encodeURIComponent(clientId) +
                     '&state=' + encodeURIComponent(state) +
                     '&scope=' + encodeURIComponent(scope);
      });

      it("should create the url with the options", function() {
        expect(App.oauth.authUri()).toEqual(authorizeUri);
      });
    });

    describe("Authorization Grant authorize uri", function() {
      beforeEach(function() {
        authorizeUri = authBaseUri;
        authorizeUri += '?response_type=' + encodeURIComponent(App.oauth_auth_code.responseType) +
                     '&redirect_uri=' + encodeURIComponent(redirectUri) +
                     '&client_id=' + encodeURIComponent(clientId) +
                     '&state=' + encodeURIComponent(state) +
                     '&scope=' + encodeURIComponent(scope);
      });

      it("should create the url with the options", function() {
        expect(App.oauth_auth_code.authUri()).toEqual(authorizeUri);
      });
    });
  });

  describe("handleRedirect", function() {
    // im;icit grant flow
    describe("Implicit grant", function() {
      describe("parse the access token from the callback url", function() {
        it("should return the params from the callback url", function() {
          expect(App.oauth.parseCallback(callbackUri)).toEqual({ access_token : '12345abc', token_type : 'Bearer', expires_in : '3600', state : '12345' });
        });
      });

      describe("redirect", function(done) {
        it("should checkState, verify the token and trigger success", function() {
          var callback = sinon.spy();
          App.oauth.on('success', callback);
          var saveToken = sinon.spy(App.oauth, "saveToken");
          var stub = sinon.stub(App.oauth, 'checkState', function() { return true; });

          App.oauth.trigger('redirect', callbackUri);

          expect(saveToken.called).toBeTruthy();
          callback.reset();
        });

        it("should trigger error when access_token is not in the callback", function() {
          var callback = sinon.spy();
          App.oauth.on('error', callback);
          App.oauth.trigger('redirect', callbackUriError);
          expect(callback.called).toBeTruthy();
          callback.reset();
        });
      });
    });

    describe("Authorization Grant", function() {
      beforeEach(function() {
        App.oauth.set('responseType', 'code');
        callbackUri = redirectUri + '#code=12345abcd' + '&state=' + state;
      });

      describe("parse the code and state from the callback url", function() {
        it("should return the params from the callback url", function() {
          expect(App.oauth.parseCallback(callbackUri)).toEqual({ code: '12345abcd', state: state });
        });
      });

      describe("redirect", function(done) {
        it("should checkState and trigger success", function() {
          var callback = sinon.spy();
          App.oauth.on('success', callback);
          var stub = sinon.stub(App.oauth, 'checkState', function() { return true; });

          App.oauth.trigger('redirect', callbackUri);

          expect(callback.called).toBeTruthy();
          callback.reset();
        });

        it("should trigger error when code is not in the callback", function() {
          var callback = sinon.spy();
          App.oauth.on('error', callback);
          App.oauth.trigger('redirect', callbackUriError);
          expect(callback.called).toBeTruthy();
          callback.reset();
        });
      });
    });

    describe("Redirect callback", function() {
      it("should call the callback if defined", function() {
        var callback = sinon.spy();
        App.oauth.trigger('redirect', callbackUriError, callback);
        expect(callback.called).toBeTruthy();
      });
    });
  });

  // saving and removing state to localStorage
  describe("localStorage state", function() {
    it("should save the state to localStorage", function() {
      var spy = sinon.spy(localStorage, 'setItem');
      App.oauth.saveState(savedState);
      expect(spy.called).toBeTruthy();
      spy.reset();
    });

    it("returns the localStorage by state", function() {
      App.oauth.saveState(savedState);
      expect(App.oauth.getState()).toEqual(savedState);
    });

    it("returns null if there is no state object in the localStorage", function() {
      expect(App.oauth.getState()).toEqual(null);
    });

    it("should remove the localStorage state after retreiving", function() {
      App.oauth.saveState(savedState);
      App.oauth.getState();
      expect(App.oauth.getState()).toEqual(null);
    });

    it("should remove any saved states", function() {
      App.oauth.saveState(savedState);

      var newState = "99999";
      var newSavedState = $.extend(true, {}, savedState);
      newSavedState.state = newState;
      App.oauth.saveState(newSavedState);

      App.oauth.clearStates();
      expect(App.oauth.getState()).toEqual(null);
    });
  });

  // checkState checks that the state returned by the OAuth server is the same as the one sent
  describe("Check the state to make sure it is set", function() {
    it("should throw an error when there is no state set", function() {
      expect(function() {App.oauth.checkState(null);}).toThrow(new Error("Could not find state."));
    });

    it("should throw an Error when the states are not equal", function() {
      var badState = $.extend(true, {}, savedState);
      badState.state = '999999';
      expect(function() { App.oauth.checkState(badState); }).toThrow(new Error("State returned from the server did not match the local saved state."));
    });

    it("should not throw an Error when the states are equal", function() {
      expect(App.oauth.checkState(savedState)).toBeTruthy();
    });
  });

  describe("Handle the saving and getting the token", function() {
    var token;
    var spyExpires;
    beforeEach(function() {
      token = { provider_id: providerId, expires_in: '12345', scope: scope, access_token: '12345abc' };
      spyExpires = sinon.stub(App.oauth, 'expiresIn', function() { return '12345'; });
      App.oauth.saveToken(token);
    });

    afterEach(function() {
      spyExpires.reset();
    });

    it("should generate the token that will be saved to the localStorage", function() {
      var params = App.oauth.parseCallback(callbackUri);
      expect(App.oauth.generateToken(params, savedState)).toEqual(token);
    });

    it("should save the token to the localStorage", function() {
      var spy = sinon.spy(localStorage, 'getItem');
      expect(App.oauth.getToken()).toEqual(token);
      spy.reset();
    });

    it("should return the access_token from the localStorage", function() {
      expect(App.oauth.getAccessToken()).toEqual('12345abc');
    });

    it("access token is expired", function() {
      expect(App.oauth.accessTokenIsExpired()).toBeTruthy();
      spyExpires.reset();
    });

    it("access token is not expired", function() {
      spyExpires = sinon.stub(App.oauth, 'now', function() { return 5; });
      expect(App.oauth.accessTokenIsExpired()).toBeFalsy();
      spyExpires.reset();
    });

    it("expires the access token", function() {
      var new_time = App.oauth.now() + 10000;
      token.expires_in = new_time;
      App.oauth.saveToken(token);
      expect(App.oauth.accessTokenIsExpired()).toBeFalsy();
      App.oauth.expireAccessToken();
      expect(App.oauth.accessTokenIsExpired()).toBeTruthy();
    });
  });

});
<commit_msg>checks that the states match for authorization grant<commit_after>describe("ember-oauth2", function() {

  window.App = Ember.Application.create({
    NAME: 'test-app'
  });

  var authorizeUri;
  var callbackUri;
  var savedState;

  var providerId, authBaseUri, callbackUriError, redirectUri, clientId, scope, state;

  var OAuth2;

  beforeEach(function() {
    providerId = 'test_auth';
    authBaseUri = 'https://foobar.dev/oauth/authorize';
    redirectUri = 'https://qux.dev/oauth/callback';
    clientId = '12345';
    scope = 'public';
    state = '12345';

    window.EmberENV = {};
    window.ENV = window.ENV || {};
    window.ENV['ember-oauth2'] = {
      test_auth: {
        clientId: clientId,
        authBaseUri: authBaseUri,
        redirectUri: redirectUri,
        scope: scope,
        state: state
      },
      test_auth_code: {
        clientId: clientId,
        authBaseUri: authBaseUri,
        redirectUri: redirectUri,
        responseType: 'code',
        scope: scope,
        state: state
      },
      test_auth_config: {
        clientId: clientId,
        authBaseUri: authBaseUri,
        redirectUri: redirectUri,
        scope: scope,
        state: state,
        statePrefix: 'foo',
        tokenPrefix: 'bar'
      }
    };

    OAuth2 = require('ember-oauth2')['default'];

    // setup test_auth
    App.oauth = OAuth2.create({providerId: providerId});

    // setup test_auth_config
    App.oauth_auth_config = OAuth2.create({providerId: 'test_auth_config'});

    // setup test_auth_code
    App.oauth_auth_code = OAuth2.create({providerId: 'test_auth_code'});

    callbackUri = redirectUri;
    callbackUri += '#access_token=' + ('12345abc') +
                '&token_type=' + 'Bearer' +
                '&expires_in=' + '3600' +
                '&state=' + state;

    callbackUriError = redirectUri + '?state=foobarqux';

    savedState = {
      provider_id: providerId,
      response_type: 'token',
      state: state,
      client_id: clientId,
      scope: scope
    };
  });

  afterEach(function() {
    authorizeUri = null;
  });

  var once = function(fn) {
    var returnValue, called = false;
    return function () {
      if (!called) {
          called = true;
          returnValue = fn.apply(this, arguments);
      }
      return returnValue;
    };
  };

  /* config test */
  describe("ENV['ember-oauth2'] namespace should exist", function() {
    it("should create a ENV object", function() {
      expect(window.ENV).toBeDefined();
    });

    it("should create a ENV['ember-oauth2] ", function() {
      expect(window.ENV['ember-oauth2']).toBeDefined();
    });
  });

  describe("initialize", function() {
    it("should be initialized with the properties of provider", function() {
      expect(App.oauth.providerId).toEqual('test_auth');
      expect(App.oauth.clientId).toEqual(clientId);
      expect(App.oauth.authBaseUri).toEqual(authBaseUri);
      expect(App.oauth.redirectUri).toEqual(redirectUri);
      expect(App.oauth.scope).toEqual(scope);
      expect(App.oauth.state).toEqual(state);
      expect(App.oauth.responseType).toEqual('token');
      expect(App.oauth.statePrefix).toEqual('state');
      expect(App.oauth.tokenPrefix).toEqual('token');
      expect(App.oauth.config).toEqual(window.ENV['ember-oauth2']);
      expect(App.oauth.providerConfig).toEqual(window.ENV['ember-oauth2']['test_auth']);
    });

    it("should set a custom state prefix", function() {
      expect(App.oauth_auth_config.statePrefix).toEqual('foo');
    });

    it("should set a custom token prefix", function() {
      expect(App.oauth_auth_config.tokenPrefix).toEqual('bar');
    });

    it("set the responseType to 'code'", function() {
      expect(App.oauth_auth_code.responseType).toEqual('code');
    });
  });

  // config errors
  describe("Errors when configuration is incomplete", function() {
    it("throws an error when there is no configuration", function() {
      window.ENV = undefined;
      expect(function() {OAuth2.create();}).toThrow(new Error('Cannot find the ember-oauth2 config.'));
    });

    it("should require a providerId in the config", function() {
      expect(function() {OAuth2.create({providerId: 'noProviderIdWithName'});}).toThrow(new Error("Cannot find the providerId: 'noProviderIdWithName' in the config."));
    });

    it("should require a clientId", function() {
      window.ENV['ember-oauth2'].test_auth.clientId = null;
      App.oauth = OAuth2.create({providerId: providerId});
      expect(function() {App.oauth.authorize();}).toThrow(new Error("No client id given."));
    });

    it("should require a authorization base uri", function() {
      window.ENV['ember-oauth2'].test_auth.authBaseUri = null;
      App.oauth = OAuth2.create({providerId: providerId});
      expect(function() {App.oauth.authorize();}).toThrow(new Error("No auth base uri given."));
    });

    it("should require a callback uri", function() {
      window.ENV['ember-oauth2'].test_auth.redirectUri = null;
      App.oauth = OAuth2.create({providerId: providerId});
      expect(function() {App.oauth.authorize();}).toThrow(new Error("No redirect uri given."));
    });
  });

  // dialog window
  describe("Dialog window returns an Ember.RSVP.Promise", function() {
    var promise;
    var errorMessage;
    beforeEach(function() {
      errorMessage = 'error message.';
      App.oauth = OAuth2.create({providerId: providerId});
    });

    it("should resolve on success", function(done){
      promise = App.oauth.openWindow();
      promise.then(function(dialog) {
        expect(dialog).toBeDefined();
        dialog.close();
      }, function(error) {
        expect(error.message).not.toBeDefined();
      }).finally(done);
    });

    it("should error when dialog does not open", function(done) {
      var stub = sinon.stub(window, 'open').returns(false);
      promise = App.oauth.openWindow();
      promise.then(function(dialog) {
        expect(dialog).not.toBeDefined();
      }, function(error) {
        expect(error.message).toEqual('Opening dialog login window failed.');
      }).finally(done);
      stub.reset();
    });
  });

  // generate oauth2 url
  describe("Generate OAuth2 providers url", function() {
    describe("Implicit Grant authorize uri", function() {
      beforeEach(function() {
        authorizeUri = authBaseUri;
        authorizeUri += '?response_type=' + encodeURIComponent(App.oauth.responseType) +
                     '&redirect_uri=' + encodeURIComponent(redirectUri) +
                     '&client_id=' + encodeURIComponent(clientId) +
                     '&state=' + encodeURIComponent(state) +
                     '&scope=' + encodeURIComponent(scope);
      });

      it("should create the url with the options", function() {
        expect(App.oauth.authUri()).toEqual(authorizeUri);
      });
    });

    describe("Authorization Grant authorize uri", function() {
      beforeEach(function() {
        authorizeUri = authBaseUri;
        authorizeUri += '?response_type=' + encodeURIComponent(App.oauth_auth_code.responseType) +
                     '&redirect_uri=' + encodeURIComponent(redirectUri) +
                     '&client_id=' + encodeURIComponent(clientId) +
                     '&state=' + encodeURIComponent(state) +
                     '&scope=' + encodeURIComponent(scope);
      });

      it("should create the url with the options", function() {
        expect(App.oauth_auth_code.authUri()).toEqual(authorizeUri);
      });
    });
  });

  describe("handleRedirect", function() {
    // im;icit grant flow
    describe("Implicit grant", function() {
      describe("parse the access token from the callback url", function() {
        it("should return the params from the callback url", function() {
          expect(App.oauth.parseCallback(callbackUri)).toEqual({ access_token : '12345abc', token_type : 'Bearer', expires_in : '3600', state : '12345' });
        });
      });

      describe("redirect", function(done) {
        it("should checkState, verify the token and trigger success", function() {
          var callback = sinon.spy();
          App.oauth.on('success', callback);
          var saveToken = sinon.spy(App.oauth, "saveToken");
          var stub = sinon.stub(App.oauth, 'checkState', function() { return true; });

          App.oauth.trigger('redirect', callbackUri);

          expect(saveToken.called).toBeTruthy();
          callback.reset();
        });

        it("should trigger error when access_token is not in the callback", function() {
          var callback = sinon.spy();
          App.oauth.on('error', callback);
          App.oauth.trigger('redirect', callbackUriError);
          expect(callback.called).toBeTruthy();
          callback.reset();
        });
      });
    });

    describe("Authorization Grant", function() {
      beforeEach(function() {
        App.oauth.set('responseType', 'code');
        callbackUri = redirectUri + '#code=12345abcd' + '&state=' + state;
      });

      describe("parse the code and state from the callback url", function() {
        it("should return the params from the callback url", function() {
          expect(App.oauth.parseCallback(callbackUri)).toEqual({ code: '12345abcd', state: state });
        });
      });

      describe("redirect", function(done) {
        it("should checkState and trigger success", function() {
          var callback = sinon.spy();
          App.oauth.on('success', callback);
          var stub = sinon.stub(App.oauth, 'checkState', function() { return true; });

          App.oauth.trigger('redirect', callbackUri);

          expect(callback.called).toBeTruthy();
          callback.reset();
        });

        it("should trigger error when the states do not match", function() {
          var callback = sinon.spy();
          App.oauth.on('error', callback);
          App.oauth.trigger('redirect', callbackUriError);
          expect(callback.called).toBeTruthy();
          callback.reset();
        });
      });
    });

    describe("Redirect callback", function() {
      it("should call the callback if defined", function() {
        var callback = sinon.spy();
        App.oauth.trigger('redirect', callbackUriError, callback);
        expect(callback.called).toBeTruthy();
      });
    });
  });

  // saving and removing state to localStorage
  describe("localStorage state", function() {
    it("should save the state to localStorage", function() {
      var spy = sinon.spy(localStorage, 'setItem');
      App.oauth.saveState(savedState);
      expect(spy.called).toBeTruthy();
      spy.reset();
    });

    it("returns the localStorage by state", function() {
      App.oauth.saveState(savedState);
      expect(App.oauth.getState()).toEqual(savedState);
    });

    it("returns null if there is no state object in the localStorage", function() {
      expect(App.oauth.getState()).toEqual(null);
    });

    it("should remove the localStorage state after retreiving", function() {
      App.oauth.saveState(savedState);
      App.oauth.getState();
      expect(App.oauth.getState()).toEqual(null);
    });

    it("should remove any saved states", function() {
      App.oauth.saveState(savedState);

      var newState = "99999";
      var newSavedState = $.extend(true, {}, savedState);
      newSavedState.state = newState;
      App.oauth.saveState(newSavedState);

      App.oauth.clearStates();
      expect(App.oauth.getState()).toEqual(null);
    });
  });

  // checkState checks that the state returned by the OAuth server is the same as the one sent
  describe("Check the state to make sure it is set", function() {
    it("should throw an error when there is no state set", function() {
      expect(function() {App.oauth.checkState(null);}).toThrow(new Error("Could not find state."));
    });

    it("should throw an Error when the states are not equal", function() {
      var badState = $.extend(true, {}, savedState);
      badState.state = '999999';
      expect(function() { App.oauth.checkState(badState); }).toThrow(new Error("State returned from the server did not match the local saved state."));
    });

    it("should not throw an Error when the states are equal", function() {
      expect(App.oauth.checkState(savedState)).toBeTruthy();
    });
  });

  describe("Handle the saving and getting the token", function() {
    var token;
    var spyExpires;
    beforeEach(function() {
      token = { provider_id: providerId, expires_in: '12345', scope: scope, access_token: '12345abc' };
      spyExpires = sinon.stub(App.oauth, 'expiresIn', function() { return '12345'; });
      App.oauth.saveToken(token);
    });

    afterEach(function() {
      spyExpires.reset();
    });

    it("should generate the token that will be saved to the localStorage", function() {
      var params = App.oauth.parseCallback(callbackUri);
      expect(App.oauth.generateToken(params, savedState)).toEqual(token);
    });

    it("should save the token to the localStorage", function() {
      var spy = sinon.spy(localStorage, 'getItem');
      expect(App.oauth.getToken()).toEqual(token);
      spy.reset();
    });

    it("should return the access_token from the localStorage", function() {
      expect(App.oauth.getAccessToken()).toEqual('12345abc');
    });

    it("access token is expired", function() {
      expect(App.oauth.accessTokenIsExpired()).toBeTruthy();
      spyExpires.reset();
    });

    it("access token is not expired", function() {
      spyExpires = sinon.stub(App.oauth, 'now', function() { return 5; });
      expect(App.oauth.accessTokenIsExpired()).toBeFalsy();
      spyExpires.reset();
    });

    it("expires the access token", function() {
      var new_time = App.oauth.now() + 10000;
      token.expires_in = new_time;
      App.oauth.saveToken(token);
      expect(App.oauth.accessTokenIsExpired()).toBeFalsy();
      App.oauth.expireAccessToken();
      expect(App.oauth.accessTokenIsExpired()).toBeTruthy();
    });
  });

});
<|endoftext|>