Source: NextGeneration/wifiBasedNativeMock.js

'use strict';

var Promise = require('lie');
var ThaliWifiInfrastructure = require('ThaliWifiInfrastructure');


/** @module WifiBasedNativeMock */

/**
 * @file
 *
 * This is a mock of {@link module:thaliMobileNative}. It is intended to
 * replicate all the capabilities of {@link module:thaliMobileNative} so that we
 * can build and test code intended to use {@link module:thaliMobileNative} but
 * on the desktop.
 *
 * We are intentionally replicating the lowest layer of the stack in order to
 * to be able to test on the desktop all the layers on top of it. This includes
 * emulating behaviors unique to iOS and Android.
 *
 * For testing purposes if callNative or registerToNative do not get all the
 * parameters they were expecting then a "Bad Arguments" exception MUST be
 * thrown.
 */

/**
 * This is the method that actually handles processing the native requests. In
 * general this method just records the arguments for later use.
 *
 * @param {string} mobileMethodName This is the name of the method that was
 * passed in on the mobile object
 * @param {platformChoice} platform
 * @param {Object} router
 * @param {WifiBasedNativeMock} wifiBasedNativeMock
 * @constructor
 */
function MobileCallInstance(mobileMethodName, platform, router,
                            wifiBasedNativeMock) {
  this.mobileMethodName = mobileMethodName;
  this.platform = platform;
  this.router = router;
  this.wifiBasedNativeMock = wifiBasedNativeMock;
}

MobileCallInstance.prototype.wifiBasedNativeMock = null;
MobileCallInstance.prototype.mobileMethodName = null;
MobileCallInstance.prototype.platform = null;
MobileCallInstance.prototype.router = null;

/**
 * In effect this listens for SSDP:alive and SSDP:byebye messages along with
 * the use of SSDP queries to find out who is around. These will be translated
 * to peer availability callbacks as specified below. This code MUST meet the
 * same requirements for using a unique SSDP port, syntax for requests, etc. as
 * {@link module:ThaliWifiInfrastructure}.
 *
 * Other requirements for this method MUST match those of {@link
 * external:"Mobile('startListeningForAdvertisements')".callNative} in terms of
 * idempotency. This also means we MUST return "Radio Turned Off" if we are
 * emulating Bluetooth as being off.
 *
 * @public
 * @param {module:thaliMobileNative~ThaliMobileCallback} callBack
 */
MobileCallInstance.prototype.startListeningForAdvertisements =
  function(callBack) {
  };

/**
 * This shuts down the SSDP listener/query code. It MUST otherwise behave as
 * given for {@link
 * external:"Mobile('stopListeningForAdvertisements')".callNative}.
 *
 * @public
 * @param {module:thaliMobileNative~ThaliMobileCallback} callBack
 */
MobileCallInstance.prototype.stopListeningForAdvertisements =
  function(callBack) {
  };

/**
 * This method tells the system to both start advertising and to accept
 * incoming connections. In both cases we need to accept incoming connections.
 * The main challenge is simulating what happens when stop is called. This is
 * supposed to shut down all incoming connections. So we can't just advertise
 * our 127.0.0.1 port and let the other mocks running on the same machine
 * connect since stop wouldn't behave properly. To handle the stop behavior,
 * that is to disconnect all incoming connections, we have to introduce a TCP
 * level proxy. The reason we need a TCP proxy is that we are using direct SSL
 * connections in a way that may or may not properly work through a HTTPS proxy.
 * So it's simpler to just introduce the TCP proxy. We will advertise the TCP
 * proxy's listener port in SSDP and when someone connects we will create a TCP
 * client connection to portNumber and then pipe the two connections together.
 *
 * __Open Issue:__ If we directly pipe the TCP listener socket (from connect)
 * and the TCP client socket (that we created) then will the system
 * automatically kill the pipe if either socket is killed? We need to test this.
 * If it doesn't then we just need to hook the close event and close the other
 * side of the pipe.
 *
 * __Note:__ For now we are going to not simulate the Bluetooth handshake for
 * Android. This covers the scenario where device A doesn't discover device B
 * over BLE but device B discovered device A over BLE and then connected over
 * Bluetooth. The handshake would create a simulated discovery event but we are
 * going to assume that the SSDP discovery will arrive in a timely manner and so
 * the behavior should be the same.
 *
 * For advertising we will use SSDP both to make SSDP:alive as well as to
 * answer queries as given in {@link module:ThaliWifiInfrastructure}.
 *
 * For incoming connections we will, as described above, just rely on everyone
 * running on 127.0.0.1.
 *
 * Otherwise the behavior MUST be the same as defined for (@link
 * external:"Mobile('startUpdateAdvertisingAndListening')".ca
 * llNative}. That includes returning the "Call Start!" error as appropriate as
 * well as returning "Radio Turned Off" if we are emulating Bluetooth as being
 * off.
 *
 * @param {number} portNumber
 * @param {module:thaliMobileNative~ThaliMobileCallback} callBack
 */
MobileCallInstance.prototype.startUpdateAdvertisingAndListening =
  function(portNumber, callBack) {
  };

/**
 * This function MUST behave like {@link module:ThaliWifiInfrastructure} and
 * send a proper SSDP:byebye and then stop responding to queries or sending
 * SSDP:alive messages. Otherwise it MUST act like
 * (@link external:"Mobile('stopAdvertisingAndListening')".callNative}
 * including terminating the TCP proxy and all of its connections to simulate
 * killing all incoming connections.
 *
 * @param {module:thaliMobileNative~ThaliMobileCallback} callBack
 */
MobileCallInstance.prototype.stopAdvertisingAndListening =
  function (callBack) {
  };

/**
 * All the usual restrictions on connect apply including throwing errors if
 * start listening isn't active, handling consecutive calls, etc. Please see the
 * details in {@link external:"Mobile('connect')".callNative}. In this case the
 * mock MUST keep track of the advertised IP and port for each peerIdentifier
 * and then be able to establish a TCP/IP listener on 127.0.0.1 and use a TCP
 * proxy to relay any connections to the 127.0.0.1 port to the IP address and
 * port that was advertised over SSDP. The point of all this redirection is to
 * fully simulate the native layer so we can run tests of the Wrapper and above
 * with full fidelity. This lets us do fun things like simulate turning off
 * radios as well as properly enforce behaviors such as those below that let our
 * local listener only accept one connection and simulating time outs on a
 * single peer correctly (e.g. the other side is still available but we had no
 * activity locally and so need to tear down). If setting up the outgoing TCP
 * proxy is a big enough pain we could probably figure a way around it but I'm
 * guessing that since we need it anyway for incoming connections it shouldn't
 * be a big deal.
 *
 * In the case of simulating Android we just have to make sure that at any
 * time we have exactly one outgoing connection to any peerIdentifier. So if we
 * get a second connect for the same peerIdentifier then we have to return the
 * port for the existing TCP listener we are using, even if it is connected. We
 * also need the right tear down behavior so that if the local app connection to
 * the local TCP listener (that will then relay to the remote peer's port) is
 * torn down then we tear down the connection to the remote peer and vice versa.
 *
 * On iOS we need the same behavior as Android plus we have to deal with the
 * MCSession problem. This means we have to look at the peerIdentifier, compare
 * it to the peerIdentifier that we generated at the SSDP layer and do a lexical
 * comparison. If we are lexically smaller then we have to simulate the trick
 * that iOS uses where we create a MCSession but don't establish any connections
 * over it. The MCSession is just used as a signaling mechanism to let the
 * lexically larger peer know that the lexically smaller peer wants to connect.
 * See the sections below on /ConnectToMeForMock and /IConnectedMock for
 * details.
 *
 * ## Making requests to /ConnectToMeForMock
 *
 * After we receive a connect when we are simulating iOS and the requester is
 * lexically smaller than the target peerIdentifier then we MUST make a GET
 * request to the target peer's /ConnectToMeForMock endpoint with a query
 * argument of the form "?port=x&peerIdentifier=y". The port is the port the
 * current peer wishes the target peer to connect over and the peerIdentifier is
 * the current peer's peerIdentifier.
 *
 * If we get a 400 response then we MUST return the "Connection could not be
 * established" error.
 *
 * If we get a 200 OK then we just have to wait for a /IConnectedMock request
 * to come in telling us that the remote peer has established a connection. See
 * the section below on how we handle this. Note that the usual timeout rules
 * apply so if the /IConnectedMock request does not come within the timeout
 * period the we MUST issue a "Connection wait time out" error.
 *
 * We do not include IP addresses in the request or response because we are
 * only running the mock amongst instances that are all hosted on the same box
 * and talking over 127.0.0.1.
 *
 * ## Sending responses to /ConnectToMeForMock
 *
 * If we are not currently simulating an iOS device then we MUST return a 500
 * Server Error because something really bad has happened. We do not currently
 * support simulating mixed scenarios, everyone in the test run needs to be
 * either simulating iOS or Android.
 *
 * If we are not currently listening for incoming connections then we MUST
 * return a 400 Bad Request. But we MUST also log the fact that this happened
 * since baring some nasty race conditions we really shouldn't get a call to
 * this endpoint unless we are listening.
 *
 * If we are listening then we MUST issue a peerAvailabilityChanged callback
 * and set the peerIdentifier to the value in the query argument, peerAvailable
 * to true and pleaseConnect to true. We MUST also record the port in the query
 * argument so that if we get a connect request we know what port to submit.
 *
 * In theory it's possible for us to get into a situation where we get one
 * port for a peerIdentifier in the /ConnectToMeForMock request and a different
 * port in a SSDP request. We should just publish the PeerAvailablityChanged
 * event as they come in and for internal mapping of peerIdentifier to port we
 * should just record whatever came in last. And yes, this can lead to fun race
 * conditions which is the situation in the real world too.
 *
 * ## Making requests to /IConnectedMock
 *
 * If we are simulating iOS and if we are establishing a TCP connection to a
 * remote peer then by definition we are the lexically larger peer. However the
 * iOS protocol shares our peerIdentifier with the remote peer, TCP does not. To
 * work around this anytime we are simulating iOS and have successfully
 * established a TCP connection to a remote peer we MUST issue a GET request to
 * the /IConnectedMock endpoint of the remote peer with the query string
 * "?clientPort=x&serverPort=z&peerIdentifier=y". The clientPort and serverPort
 * are the client port and server port values from the TCP connection that
 * caused us to send this request in the first place. The peerIdentifier is our
 * peerIdentifier. If we get a 400 response back then we MUST log this event as
 * it really should not have happened.
 *
 * ## Sending response to /IConnectedMock
 *
 * If we are not currently simulating an iOS device then we MUST return a 500
 * Server Error because something really bad has happened. We do not currently
 * support simulating mixed scenarios, everyone in the test run needs to be
 * either simulating iOS or Android.
 *
 * If we are not currently listening for incoming connections then we MUST
 * return a 400 Bad Request. But we MUST also log the fact that this happened
 * since baring some nasty race conditions we really shouldn't have been able to
 * set up the TCP connection in the first place.
 *
 * Otherwise we MUST return a 200 OK.
 *
 * When we return a 200 OK we MUST issue a peerAvailabilityChanged callback
 * with peerIdentifier set to the submitted peerIdentifier, peerAvailable set to
 * true and pleaseConnect set to false. If we have an outstanding connect
 * request to the specified peerIdentifier then we MUST look up the specified
 * clientPort/serverPort and see if we can match it to any of the incoming
 * connections to the TCP proxy. If we can then we MUST return the
 * clientPort/serverPort being used by the TCP proxy as the connect response
 * with listeningPort set to null and clientPort/serverPort set to the values
 * the TCP proxy is using. If we cannot match the connection via the TCP proxy
 * then this means that the connection might have died or been killed while this
 * request to /IConnectedMock was being sent. In that case we should send bogus
 * values in the connect response to simulate a situation where a peer connects
 * but then the connection dies before the connect callback is returned.
 *
 * @param {string} peerIdentifier
 * @param {module:thaliMobileNative~ConnectCallback} callback
 */
MobileCallInstance.prototype.connect = function(peerIdentifier, callback) {
};

/**
 * If we aren't emulating iOS then this method has to return the "Not
 * Supported" error. If we are emulating iOS then we have to kill all the TCP
 * listeners we are using to handling outgoing connections and the TCP proxy we
 * are using to handle incoming connections.
 *
 * @public
 * @param {module:thaliMobileNative~ThaliMobileCallback} callback
 */
MobileCallInstance.prototype.killConnections = function (callback) {
};



/**
 * Handles processing callNative requests. The actual params differ based on
 * the particular Mobile method that is being called.
 */
MobileCallInstance.prototype.callNative = function () {
  switch (this.mobileMethodName) {
    case 'startListeningForAdvertisements':
    {
      return this.startListeningForAdvertisements(arguments[0]);
    }
    case 'stopListeningForAdvertisements':
    {
      return this.stopListeningForAdvertisements(arguments[0]);
    }
    case 'startUpdateAdvertisingAndListening':
    {
      return this.startUpdateAdvertisingAndListening(
          arguments[0], arguments[1]);
    }
    case 'stopAdvertisingAndListening':
    {
      return this.stopAdvertisingAndListening(
          arguments[0]);
    }
    case 'connect':
    {
      return this.connect(arguments[0], arguments[1]);
    }
    case 'killConnections':
    {
      return this.killConnections(arguments[0]);
    }
    default:
    {
      throw new Error('The supplied mobileName does not have a matching ' +
          'callNative method: ' + this.mobileMethodName);
    }
  }
};

/**
 * Anytime we are looking for advertising and we receive a SSDP:alive,
 * SSDP:byebye or a response to one of our periodic queries we should use it to
 * create a peerAvailabilityChanged call back. In practice we don't really need
 * to batch these messages so we can just fire them as we get them. The
 * peerIdentifier is the USN from the SSDP message, peerAvailable is true or
 * false based on the SSDP response and pleaseConnect is false except for the
 * situation described above for /ConnectToMeforMock.
 *
 * @param {module:thaliMobileNative~peerAvailabilityChangedCallback} callback
 */
MobileCallInstance.prototype.peerAvailabilityChanged = function (callback) {
};

/**
 * Any time there is a call to start and stop or if Bluetooth is turned off on
 * Android (which also MUST mean that we have disabled both advertising and
 * discovery) then we MUST fire this event.
 *
 * @public
 * @param {module:thaliMobileNative~discoveryAdvertisingStateUpdateNonTCPCallback} callback
 */
MobileCallInstance.prototype.discoveryAdvertisingStateUpdateNonTCP =
    function (callback) {
    };

/**
 * At this point this event would only fire because we called toggleBluetooth
 * or toggleWifi. For the moment we will treat toggleBluetooth and turning
 * on/off both blueToothLowEnergy and blueTooth.
 *
 * __Open Issue:__ Near as I can tell both Android and iOS have a single
 * Bluetooth switch that activates and de-activates Bluetooth and BLE. Note
 * however that in theory it's possible to still have one available and not the
 * other to a particular application because of app level permissions but that
 * isn't an issue for the mock.
 *
 * @public
 * @param {module:thaliMobileNative~networkChangedCallback} callback
 */
MobileCallInstance.prototype.networkChanged = function (callback) {
};

/**
 * This is used anytime the TCP proxy for incoming connections cannot connect
 * to the portNumber set in
 * {@link module:WifiBasedNativeMock~MobileCallInstance.startUpdateAdvertisingAndListening}.
 *
 * @public
 * @param {module:thaliMobileNative~incomingConnectionToPortNumberFailedCallback} callback
 */
MobileCallInstance.prototype.incomingConnectionToPortNumberFailed =
    function (callback) {
    };

MobileCallInstance.prototype.registerToNative = function () {
  switch (this.mobileMethodName) {
    case 'peerAvailabilityChanged':
    {
      return this.peerAvailabilityChanged(arguments[0]);
    }
    case 'discoveryAdvertisingStateUpdateNonTCP':
    {
      return this.discoveryAdvertisingStateUpdateNonTCP(arguments[0]);
    }
    case 'networkChanged':
    {
      return this.networkChanged(arguments[0]);
    }
    case 'incomingConnectionToPortNumberFailed':
    {
      return this.incomingConnectionToPortNumberFailed(arguments[0]);
    }
    default:
    {
      throw new Error('The supplied mobileName does not have a matching ' +
          'registerToNative method: ' + this.mobileMethodName);
    }
  }
};

/**
 * Enum to describe the platforms we can simulate, this mostly controls how we
 * handle connect
 *
 * @public
 * @readonly
 * @enum {string}
 */
var platformChoice = {
  ANDROID: 'Android',
  IOS: 'iOS'
};

/**
 * This simulates turning Bluetooth on and off.
 *
 * If we are emulating Android then we MUST start with Bluetooth and WiFi
 * turned off.
 *
 * __Open Issue:__ I believe that JXCore will treat this as a NOOP if called
 * on iOS. We need to check and emulate their behavior.
 *
 * @param {platformChoice} platform
 * @param {ThaliWifiInfrastructure} wifiBasedNativeMock
 * @returns {Function}
 */
function toggleBluetooth (platform, wifiBasedNativeMock) {
  return function (setting, callback) {
    return null;
  };
}

/**
 * If we are on Android then then is a NOOP since we don't care (although to
 * be good little programmers we should still fire a network changed event). We
 * won't be using Wifi for discovery or connectivity in the near future.
 *
 * __Open Issue:__ I believe that JXCore will treat this as a NOOP if called
 * on iOS. We need to check and emulate their behavior.
 *
 * @param {platformChoice} platform
 * @param {ThaliWifiInfrastructure} wifiBasedNativeMock
 * @returns {Function}
 */
function toggleWiFi(platform, wifiBasedNativeMock) {
  return function (setting, callback) {
    return null;
  };
}

/**
 * To use this mock save the current global object Mobile (if it exists) and
 * replace it with this object. In general this object won't exist on the
 * desktop.
 *
 * If we are simulating iOS then we MUST add the /ConnectToMeForMock and
 * /IConnectedMock endpoints as described above to the router object.
 *
 * @public
 * @constructor
 * @param {platformChoice} platform
 * @param {Object} router This is the express router being used up in the
 * stack. We need it here so we can add a router to simulate the iOS case where
 * we need to let the other peer know we want a connection.
 */
function WifiBasedNativeMock(platform, router) {
  var thaliWifiInfrastructure = new ThaliWifiInfrastructure();
  var mobileHandler = function (mobileMethodName) {
    return new MobileCallInstance(mobileMethodName, platform, router,
                                    thaliWifiInfrastructure);
  };

  mobileHandler.toggleBluetooth = toggleBluetooth(thaliWifiInfrastructure);

  mobileHandler.toggleWiFi = toggleWiFi(thaliWifiInfrastructure);

  return mobileHandler;
}

module.exports = WifiBasedNativeMock;