hunk.code_window: 'use strict'
   const m = require('mochainon')
   const angular = require('angular')
   const electron = require('electron')
   require('angular-mocks')
   describe('Browser: OSOpenExternal', function () {
     beforeEach(angular.mock.module(
       require('../../../lib/gui/app/os/open-external/open-external')
  
hunk.labels: keep keep keep keep keep replace keep keep keep keep
hunk.file_path: tests/gui/os/open-external.spec.js
hunk.type: replace
file: /*
   * Copyright 2016 resin.io
   *
   * 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.
   */
  
  'use strict'
  
  const Immutable = require('immutable')
  const _ = require('lodash')
  const redux = require('redux')
  const uuidV4 = require('uuid/v4')
  const constraints = require('../../../shared/drive-constraints')
  const supportedFormats = require('../../../shared/supported-formats')
  const errors = require('../../../shared/errors')
  const fileExtensions = require('../../../shared/file-extensions')
  const utils = require('../../../shared/utils')
  const settings = require('./settings')
  
  /**
   * @summary Verify and throw if any state fields are nil
   * @function
   * @public
   *
   * @param {Object} object - state object
   * @param {Array<Array<String>> | Array<String>} fields - array of object field paths
   * @param {String} name - name of the state we're dealing with
   * @throws
   *
   * @example
   * const fields = [ 'type', 'percentage' ]
   * verifyNoNilFields(action.data, fields, 'flash')
   */
  const verifyNoNilFields = (object, fields, name) => {
    const nilFields = _.filter(fields, (field) => {
      return _.isNil(_.get(object, field))
    })
    if (nilFields.length) {
      throw new Error(`Missing ${name} fields: ${nilFields.join(', ')}`)
    }
  }
  
  /**
   * @summary FLASH_STATE fields that can't be nil
   * @constant
   * @private
   */
  const flashStateNoNilFields = [
    'speed',
    'totalSpeed'
  ]
  
  /**
   * @summary SELECT_IMAGE fields that can't be nil
   * @constant
   * @private
   */
  const selectImageNoNilFields = [
    'path',
    'extension'
  ]
  
  /**
   * @summary Application default state
   * @type {Object}
   * @constant
   * @private
   */
  const DEFAULT_STATE = Immutable.fromJS({
    applicationSessionUuid: '',
    flashingWorkflowUuid: '',
    availableDrives: [],
    selection: {
      devices: new Immutable.OrderedSet()
    },
    isFlashing: false,
    flashResults: {},
    flashState: {
      flashing: 0,
      verifying: 0,
      successful: 0,
      failed: 0,
      percentage: 0,
      speed: null,
      totalSpeed: null
    }
  })
  
  /**
   * @summary Application supported action messages
   * @type {Object}
   * @constant
   */
  const ACTIONS = _.fromPairs(_.map([
    'SET_AVAILABLE_DRIVES',
    'SET_FLASH_STATE',
    'RESET_FLASH_STATE',
    'SET_FLASHING_FLAG',
    'UNSET_FLASHING_FLAG',
    'SELECT_DRIVE',
    'SELECT_IMAGE',
    'DESELECT_DRIVE',
    'DESELECT_IMAGE',
    'SET_APPLICATION_SESSION_UUID',
    'SET_FLASHING_WORKFLOW_UUID',
    'SET_WEBVIEW_SHOWING_STATUS'
  ], (message) => {
    return [ message, message ]
  }))
  
  /**
   * @summary Get available drives from the state
   * @function
   * @public
   *
   * @param {Object} state - state object
   * @returns {Object} new state
   *
   * @example
   * const drives = getAvailableDrives(state)
   * _.find(drives, { device: '/dev/sda' })
   */
  const getAvailableDrives = (state) => {
    // eslint-disable-next-line lodash/prefer-lodash-method
    return state.get('availableDrives').toJS()
  }
  
  /**
   * @summary The redux store reducer
   * @function
   * @private
   *
   * @param {Object} state - application state
   * @param {Object} action - dispatched action
   * @returns {Object} new application state
   *
   * @example
   * const newState = storeReducer(DEFAULT_STATE, {
   *   type: ACTIONS.DESELECT_DRIVE
   * });
   */
  const storeReducer = (state = DEFAULT_STATE, action) => {
    switch (action.type) {
      case ACTIONS.SET_AVAILABLE_DRIVES: {
        // Type: action.data : Array<DriveObject>
  
        if (!action.data) {
          throw errors.createError({
            title: 'Missing drives'
          })
        }
  
        const drives = action.data
  
        if (!_.isArray(drives) || !_.every(drives, _.isObject)) {
          throw errors.createError({
            title: `Invalid drives: ${drives}`
          })
        }
  
        const newState = state.set('availableDrives', Immutable.fromJS(drives))
        const selectedDevices = newState.getIn([ 'selection', 'devices' ]).toJS()
  
        // Remove selected drives that are stale, i.e. missing from availableDrives
        const nonStaleNewState = _.reduce(selectedDevices, (accState, device) => {
          // Check whether the drive still exists in availableDrives
          if (device && !_.find(drives, {
            device
          })) {
            // Deselect this drive gone from availableDrives
            return storeReducer(accState, {
              type: ACTIONS.DESELECT_DRIVE,
              data: device
            })
          }
  
          return accState
        }, newState)
  
        const shouldAutoselectAll = Boolean(settings.get('disableExplicitDriveSelection'))
        const AUTOSELECT_DRIVE_COUNT = 1
        const nonStaleSelectedDevices = nonStaleNewState.getIn([ 'selection', 'devices' ]).toJS()
        const hasSelectedDevices = nonStaleSelectedDevices.length >= AUTOSELECT_DRIVE_COUNT
        const shouldAutoselectOne = drives.length === AUTOSELECT_DRIVE_COUNT && !hasSelectedDevices
  
        if (shouldAutoselectOne || shouldAutoselectAll) {
          // Even if there's no image selected, we need to call several
          // drive/image related checks, and `{}` works fine with them
          const image = state.getIn([ 'selection', 'image' ], Immutable.fromJS({})).toJS()
  
          return _.reduce(drives, (accState, drive) => {
            if (_.every([
              constraints.isDriveValid(drive, image),
              constraints.isDriveSizeRecommended(drive, image),
  
              // We don't want to auto-select large drives
              !constraints.isDriveSizeLarge(drive),
  
              // We don't want to auto-select system drives,
              // even when "unsafe mode" is enabled
              !constraints.isSystemDrive(drive)
  
            ]) || (shouldAutoselectAll && constraints.isDriveValid(drive, image))) {
              // Auto-select this drive
              return storeReducer(accState, {
                type: ACTIONS.SELECT_DRIVE,
                data: drive.device
              })
            }
  
            // Deselect this drive in case it still is selected
            return storeReducer(accState, {
              type: ACTIONS.DESELECT_DRIVE,
              data: drive.device
            })
          }, nonStaleNewState)
        }
  
        return nonStaleNewState
      }
  
      case ACTIONS.SET_FLASH_STATE: {
        // Type: action.data : FlashStateObject
  
        if (!state.get('isFlashing')) {
          throw errors.createError({
            title: 'Can\'t set the flashing state when not flashing'
          })
        }
  
        verifyNoNilFields(action.data, flashStateNoNilFields, 'flash')
  
        if (!_.every(_.pick(action.data, [
          'flashing',
          'verifying',
          'successful',
          'failed'
        ]), _.isFinite)) {
          throw errors.createError({
            title: 'State quantity field(s) not finite number'
          })
        }
  
        if (!_.isUndefined(action.data.percentage) && !utils.isValidPercentage(action.data.percentage)) {
          throw errors.createError({
            title: `Invalid state percentage: ${action.data.percentage}`
          })
        }
  
        if (!_.isUndefined(action.data.eta) && !_.isNumber(action.data.eta)) {
          throw errors.createError({
            title: `Invalid state eta: ${action.data.eta}`
          })
        }
  
        return state.set('flashState', Immutable.fromJS(action.data))
      }
  
      case ACTIONS.RESET_FLASH_STATE: {
        return state
          .set('isFlashing', false)
          .set('flashState', DEFAULT_STATE.get('flashState'))
          .set('flashResults', DEFAULT_STATE.get('flashResults'))
          .delete('flashUuid')
      }
  
      case ACTIONS.SET_FLASHING_FLAG: {
        return state
          .set('isFlashing', true)
          .set('flashUuid', uuidV4())
          .set('flashResults', DEFAULT_STATE.get('flashResults'))
      }
  
      case ACTIONS.UNSET_FLASHING_FLAG: {
        // Type: action.data : FlashResultsObject
  
        if (!action.data) {
          throw errors.createError({
            title: 'Missing results'
          })
        }
  
        _.defaults(action.data, {
          cancelled: false
        })
  
        if (!_.isBoolean(action.data.cancelled)) {
          throw errors.createError({
            title: `Invalid results cancelled: ${action.data.cancelled}`
          })
        }
  
        if (action.data.cancelled && action.data.sourceChecksum) {
          throw errors.createError({
            title: 'The sourceChecksum value can\'t exist if the flashing was cancelled'
          })
        }
  
        if (action.data.sourceChecksum && !_.isString(action.data.sourceChecksum)) {
          throw errors.createError({
            title: `Invalid results sourceChecksum: ${action.data.sourceChecksum}`
          })
        }
  
        if (action.data.errorCode && !_.isString(action.data.errorCode) && !_.isNumber(action.data.errorCode)) {
          throw errors.createError({
            title: `Invalid results errorCode: ${action.data.errorCode}`
          })
        }
  
        return state
          .set('isFlashing', false)
          .set('flashResults', Immutable.fromJS(action.data))
          .set('flashState', DEFAULT_STATE.get('flashState'))
      }
  
      case ACTIONS.SELECT_DRIVE: {
        // Type: action.data : String
  
        const device = action.data
  
        if (!device) {
          throw errors.createError({
            title: 'Missing drive'
          })
        }
  
        if (!_.isString(device)) {
          throw errors.createError({
            title: `Invalid drive: ${device}`
          })
        }
  
        const selectedDrive = _.find(getAvailableDrives(state), { device })
  
        if (!selectedDrive) {
          throw errors.createError({
            title: `The drive is not available: ${device}`
          })
        }
  
        if (selectedDrive.isReadOnly) {
          throw errors.createError({
            title: 'The drive is write-protected'
          })
        }
  
        const image = state.getIn([ 'selection', 'image' ])
        if (image && !constraints.isDriveLargeEnough(selectedDrive, image.toJS())) {
          throw errors.createError({
            title: 'The drive is not large enough'
          })
        }
  
        const selectedDevices = state.getIn([ 'selection', 'devices' ])
  
        return state.setIn([ 'selection', 'devices' ], selectedDevices.add(device))
      }
  
      // TODO(jhermsmeier): Consolidate these assertions
      // with image-stream / supported-formats, and have *one*
      // place where all the image extension / format handling
      // takes place, to avoid having to check 2+ locations with different logic
      case ACTIONS.SELECT_IMAGE: {
        // Type: action.data : ImageObject
  
        verifyNoNilFields(action.data, selectImageNoNilFields, 'image')
  
        if (!_.isString(action.data.path)) {
          throw errors.createError({
            title: `Invalid image path: ${action.data.path}`
          })
        }
  
        if (!_.isString(action.data.extension)) {
          throw errors.createError({
            title: `Invalid image extension: ${action.data.extension}`
          })
        }
  
        const extension = _.toLower(action.data.extension)
  
        if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
          throw errors.createError({
            title: `Invalid image extension: ${action.data.extension}`
          })
        }
  
        let lastImageExtension = fileExtensions.getLastFileExtension(action.data.path)
        lastImageExtension = _.isString(lastImageExtension) ? _.toLower(lastImageExtension) : lastImageExtension
  
        if (lastImageExtension !== extension) {
          if (!_.isString(action.data.archiveExtension)) {
            throw errors.createError({
              title: 'Missing image archive extension'
            })
          }
  
          const archiveExtension = _.toLower(action.data.archiveExtension)
  
          if (!_.includes(supportedFormats.getAllExtensions(), archiveExtension)) {
            throw errors.createError({
              title: `Invalid image archive extension: ${action.data.archiveExtension}`
            })
          }
  
          if (lastImageExtension !== archiveExtension) {
            throw errors.createError({
              title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`
            })
          }
        }
  
        const MINIMUM_IMAGE_SIZE = 0
  
        // eslint-disable-next-line no-undefined
        if (action.data.size !== undefined) {
          if ((action.data.size < MINIMUM_IMAGE_SIZE) || !_.isInteger(action.data.size)) {
            throw errors.createError({
              title: `Invalid image size: ${action.data.size}`
            })
          }
        }
  
        if (!_.isUndefined(action.data.compressedSize)) {
          if ((action.data.compressedSize < MINIMUM_IMAGE_SIZE) || !_.isInteger(action.data.compressedSize)) {
            throw errors.createError({
              title: `Invalid image compressed size: ${action.data.compressedSize}`
            })
          }
        }
  
        if (action.data.url && !_.isString(action.data.url)) {
          throw errors.createError({
            title: `Invalid image url: ${action.data.url}`
          })
        }
  
        if (action.data.name && !_.isString(action.data.name)) {
          throw errors.createError({
            title: `Invalid image name: ${action.data.name}`
          })
        }
  
        if (action.data.logo && !_.isString(action.data.logo)) {
          throw errors.createError({
            title: `Invalid image logo: ${action.data.logo}`
          })
        }
  
        const selectedDevices = state.getIn([ 'selection', 'devices' ])
  
        // Remove image-incompatible drives from selection with `constraints.isDriveValid`
        return _.reduce(selectedDevices.toJS(), (accState, device) => {
          const drive = _.find(getAvailableDrives(state), { device })
          if (!constraints.isDriveValid(drive, action.data) || !constraints.isDriveSizeRecommended(drive, action.data)) {
            return storeReducer(accState, {
              type: ACTIONS.DESELECT_DRIVE,
              data: device
            })
          }
  
          return accState
        }, state).setIn([ 'selection', 'image' ], Immutable.fromJS(action.data))
      }
  
      case ACTIONS.DESELECT_DRIVE: {
        // Type: action.data : String
  
        if (!action.data) {
          throw errors.createError({
            title: 'Missing drive'
          })
        }
  
        if (!_.isString(action.data)) {
          throw errors.createError({
            title: `Invalid drive: ${action.data}`
          })
        }
  
        const selectedDevices = state.getIn([ 'selection', 'devices' ])
  
        // Remove drive from set in state
        return state.setIn([ 'selection', 'devices' ], selectedDevices.delete(action.data))
      }
  
      case ACTIONS.DESELECT_IMAGE: {
        return state.deleteIn([ 'selection', 'image' ])
      }
  
      case ACTIONS.SET_APPLICATION_SESSION_UUID: {
        return state.set('applicationSessionUuid', action.data)
      }
  
      case ACTIONS.SET_FLASHING_WORKFLOW_UUID: {
        return state.set('flashingWorkflowUuid', action.data)
      }
  
      case ACTIONS.SET_WEBVIEW_SHOWING_STATUS: {
        return state.set('isWebviewShowing', action.data)
      }
  
      default: {
        return state
      }
    }
  }
  
  module.exports = _.merge(redux.createStore(storeReducer, DEFAULT_STATE), {
    Actions: ACTIONS,
    Defaults: DEFAULT_STATE
  })
  
  /**
   * @summary Observe the store for changes
   * @param {Function} onChange - change handler
   * @returns {Function} unsubscribe
   * @example
   * store.observe((newState) => {
   *   // ...
   * })
   */
  module.exports.observe = (onChange) => {
    let currentState = null
  
    /**
     * @summary Internal change detection handler
     * @private
     * @example
     * store.subscribe(changeHandler)
     */
    const changeHandler = () => {
      const nextState = module.exports.getState()
      if (!_.isEqual(nextState, currentState)) {
        currentState = nextState
        onChange(currentState)
      }
    }
  
    changeHandler()
  
    return module.exports.subscribe(changeHandler)
  }
  