<commit_before>/*
 *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
const chai = require('chai');
const expect = chai.expect;
const sinon = require('sinon');
chai.use(require('dirty-chai'));
chai.use(require('sinon-chai'));

const mockORTC = require('./ortcmock');
const mockGetUserMedia = require('./gummock');
const shimPeerConnection = require('../rtcpeerconnection');
const SDPUtils = require('sdp');

const FINGERPRINT_SHA256 = '8C:71:B3:8D:A5:38:FD:8F:A4:2E:A2:65:6C' +
    ':fd00:a516:7c1b:17cd:6d81:2137:bd2a:2c5b:7C:4D:B5:DF:AF:AA:6F:44:90:8D:F4';
const ICEUFRAG = 'someufrag';
const ICEPWD = '<PASSWORD>';
const SDP_BOILERPLATE = 'v=0\r\n' +
    'o=- 166855176514521964 2 IN IP4 127.0.0.1\r\n' +
    's=-\r\n' +
    't=0 0\r\n' +
    'a=msid-semantic:WMS *\r\n';
const MINIMAL_AUDIO_MLINE =
    'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
    'c=IN IP4 0.0.0.0\r\n' +
    'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
    'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
    'a=ice-pwd:' + ICEPWD + '\r\n' +
    'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
    'a=setup:actpass\r\n' +
    'a=mid:audio1\r\n' +
    'a=sendonly\r\n' +
    'a=rtcp-mux\r\n' +
    'a=rtcp-rsize\r\n' +
    'a=rtpmap:111 opus/48000/2\r\n' +
    'a=ssrc:1001 cname:some\r\n';


// this detects that we are not running in a browser.
const mockWindow = typeof window === 'undefined';

describe('Edge shim', () => {
  let RTCPeerConnection;
  beforeEach(() => {
    if (mockWindow) {
      global.window = {setTimeout};
      mockGetUserMedia(window);
      mockORTC(window);
      global.navigator = window.navigator;
    }
    RTCPeerConnection = shimPeerConnection(window, 15025);
  });

  beforeEach(() => {
    let streams = [];
    let release = () => {
      streams.forEach((stream) => {
        stream.getTracks().forEach((track) => {
          track.stop();
        });
      });
      streams = [];
    };

    let origGetUserMedia = navigator.getUserMedia.bind(navigator);
    navigator.getUserMedia = (constraints, cb, eb) => {
      origGetUserMedia(constraints, (stream) => {
        streams.push(stream);
        if (cb) {
          cb.apply(null, [stream]);
        }
      }, eb);
    };
    navigator.getUserMedia.restore = () => {
      navigator.getUserMedia = origGetUserMedia;
      release();
    };

    let origMediaDevicesGetUserMedia =
        navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
    navigator.mediaDevices.getUserMedia = (constraints) => {
      return origMediaDevicesGetUserMedia(constraints, (stream) => {
        streams.push(stream);
        return stream;
      });
    };
    navigator.mediaDevices.getUserMedia.restore = () => {
      navigator.mediaDevices.getUserMedia = origMediaDevicesGetUserMedia;
      release();
    };
  });

  afterEach(() => {
    navigator.getUserMedia.restore();
    navigator.mediaDevices.getUserMedia.restore();
  });

  describe('RTCPeerConnection constructor', () => {
    it('throws a NotSupportedError when called with ' +
        'rtcpMuxPolicy negotiate', () => {
      const constructor = () => {
        return new RTCPeerConnection({rtcpMuxPolicy: 'negotiate'});
      };
      expect(constructor).to.throw(/rtcpMuxPolicy/)
          .that.has.property('name').that.equals('NotSupportedError');
    });

    describe('when RTCIceCandidatePoolSize is set', () => {
      beforeEach(() => {
        sinon.spy(window, 'RTCIceGatherer');
      });

      afterEach(() => {
        window.RTCIceGatherer.restore();
      });

      it('creates an ICE Gatherer', () => {
        new RTCPeerConnection({iceCandidatePoolSize: 1});
        expect(window.RTCIceGatherer).to.have.been.calledOnce();
      });

      // TODO: those tests are convenient because they are sync and
      //    dont require createOffer-SLD before creating the gatherer.
      it('sets default ICETransportPolicy on RTCIceGatherer', () => {
        new RTCPeerConnection({iceCandidatePoolSize: 1});
        expect(window.RTCIceGatherer).to.have.been.calledWith(sinon.match({
          gatherPolicy: 'all'
        }));
      });

      it('sets ICETransportPolicy=all on RTCIceGatherer', () => {
        new RTCPeerConnection({iceCandidatePoolSize: 1,
            iceTransportPolicy: 'all'});
        expect(window.RTCIceGatherer).to.have.been.calledWith(sinon.match({
          gatherPolicy: 'all'
        }));
      });
      it('sets ICETransportPolicy=relay on RTCIceGatherer', () => {
        new RTCPeerConnection({iceCandidatePoolSize: 1,
            iceTransportPolicy: 'relay'});
        expect(window.RTCIceGatherer).to.have.been.calledWith(sinon.match({
          gatherPolicy: 'relay'
        }));
      });
    });
  });

  describe('setLocalDescription', () => {
    let pc;
    beforeEach(() => {
      pc = new RTCPeerConnection();
    });
    afterEach(() => {
      pc.close();
    });

    it('returns a promise', (done) => {
      pc.createOffer({offerToReceiveAudio: 1})
      .then((offer) => {
        return pc.setLocalDescription(offer);
      })
      .then(done);
    });

    it('calls the legacy success callback', (done) => {
      pc.createOffer({offerToReceiveAudio: 1})
      .then((offer) => {
        return pc.setLocalDescription(offer, done, () => {});
      });
    });

    it('changes the signalingState to have-local-offer', (done) => {
      pc.createOffer({offerToReceiveAudio: 1})
      .then((offer) => {
        return pc.setLocalDescription(offer);
      })
      .then(() => {
        expect(pc.localDescription.type).to.equal('offer');
        expect(pc.signalingState = 'have-local-offer');
        done();
      });
    });

    describe('InvalidStateError is thrown when called with', () => {
      const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
      it('an offer in signalingState have-remote-offer', (done) => {
        pc.setRemoteDescription({type: 'offer', sdp})
        .then(() => {
          return pc.setLocalDescription({type: 'offer'});
        })
        .catch((e) => {
          expect(e.name).to.equal('InvalidStateError');
          done();
        });
      });

      it('an answer in signalingState have-local-offer', (done) => {
        pc.createOffer({offerToReceiveAudio: 1})
        .then((offer) => {
          return pc.setLocalDescription(offer);
        })
        .then(() => {
          return pc.setLocalDescription({type: 'answer'});
        })
        .catch((e) => {
          expect(e.name).to.equal('InvalidStateError');
          done();
        });
      });
    });

    describe('starts emitting ICE candidates', () => {
      let clock;
      beforeEach(() => {
        clock = sinon.useFakeTimers();
      });
      afterEach(() => {
        clock.restore();
      });

      describe('calls', () => {
        it('the onicecandidate callback', (done) => {
          pc.onicegatheringstatechange = () => {
            if (pc.iceGatheringState === 'complete') {
              expect(pc.onicecandidate).to.have.been.calledWith();
              done();
            }
          };
          pc.onicecandidate = sinon.stub();
          pc.createOffer({offerToReceiveAudio: 1})
          .then(offer => pc.setLocalDescription(offer))
          .then(() => {
            window.setTimeout(() => {
              clock.tick(500);
            });
            clock.tick(0);
          });
        });
        it('the icecandidate event listener', (done) => {
          const stub = sinon.stub();
          pc.onicegatheringstatechange = () => {
            if (pc.iceGatheringState === 'complete') {
              expect(stub).to.have.been.calledWith();
              done();
            }
          };
          pc.addEventListener('icecandidate', stub);
          pc.createOffer({offerToReceiveAudio: 1})
          .then(offer => pc.setLocalDescription(offer))
          .then(() => {
            window.setTimeout(() => {
              clock.tick(500);
            });
            clock.tick(0);
          });
        });
      });

      it('updates localDescription.sdp with candidates', (done) => {
        pc.onicegatheringstatechange = () => {
          if (pc.iceGatheringState === 'complete') {
            expect(SDPUtils.matchPrefix(pc.localDescription.sdp,
                'a=candidate:').length).to.be.above(0);
            expect(SDPUtils.matchPrefix(pc.localDescription.sdp,
                'a=end-of-candidates')).to.have.length(1);
            done();
          }
        };
        pc.createOffer({offerToReceiveAudio: 1})
        .then(offer => pc.setLocalDescription(offer))
        .then(() => {
          window.setTimeout(() => {
            clock.tick(500);
          });
          clock.tick(0);
        });
      });

      it('changes iceGatheringState and emits icegatheringstatechange ' +
          'event', (done) => {
        let states = [];
        pc.addEventListener('icegatheringstatechange', () => {
          states.push(pc.iceGatheringState);
          if (pc.iceGatheringState === 'complete') {
            expect(states.length).to.equal(2);
            expect(states).to.contain('gathering');
            expect(states).to.contain('complete');
            done();
          }
        });
        pc.createOffer({offerToReceiveAudio: 1})
        .then(offer => pc.setLocalDescription(offer))
        .then(() => {
          expect(pc.iceGatheringState).to.equal('new');
          clock.tick(500);
        });
      });
    });

    describe('after setRemoteDescription', () => {
      beforeEach(() => {
        sinon.spy(window.RTCIceTransport.prototype, 'start');
        sinon.spy(window.RTCDtlsTransport.prototype, 'start');
      });
      afterEach(() => {
        window.RTCIceTransport.prototype.start.restore();
        window.RTCDtlsTransport.prototype.start.restore();
      });

      const sdp = SDP_BOILERPLATE +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:1001 cname:some\r\n';
      it('starts the ice transport', (done) => {
        pc.setRemoteDescription({type: 'offer', sdp: sdp})
        .then(() => {
          return pc.createAnswer();
        })
        .then((answer) => {
          return pc.setLocalDescription(answer);
        })
        .then(() => {
          const receiver = pc.getReceivers()[0];
          const iceTransport = receiver.transport.transport;
          expect(iceTransport.start).to.have.been.calledOnce();
          expect(iceTransport.start).to.have.been.calledWith(
            sinon.match.any,
            sinon.match({
              usernameFragment: '' + ICEUFRAG + '',
              password: '' + <PASSWORD> + ''
            })
          );
          done();
        });
      });

      it('starts the dtls transport', (done) => {
        pc.setRemoteDescription({type: 'offer', sdp: sdp})
        .then(() => {
          return pc.createAnswer();
        })
        .then((answer) => {
          return pc.setLocalDescription(answer);
        })
        .then(() => {
          const receiver = pc.getReceivers()[0];
          const dtlsTransport = receiver.transport;
          expect(dtlsTransport.start).to.have.been.calledOnce();
          expect(dtlsTransport.start).to.have.been.calledWith(
            sinon.match({
              role: 'auto',
              fingerprints: sinon.match([
                sinon.match({
                  algorithm: 'sha-256',
                  value: FINGERPRINT_SHA256
                })
              ])
            })
          );
          done();
        });
      });
    });
  });

  describe('setRemoteDescription', () => {
    let pc;
    beforeEach(() => {
      pc = new RTCPeerConnection();
    });
    afterEach(() => {
      pc.close();
    });

    it('returns a promise', (done) => {
      const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
      pc.setRemoteDescription({type: 'offer', sdp: sdp})
      .then(done);
    });
    it('calls the legacy success callback', (done) => {
      const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
      pc.setRemoteDescription({type: 'offer', sdp: sdp}, done, () => {});
    });

    it('changes the signalingState to have-remote-offer', (done) => {
      const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
      pc.setRemoteDescription({type: 'offer', sdp: sdp})
      .then(() => {
        expect(pc.signalingState = 'have-remote-offer');
        done();
      });
    });

    it('sets the remoteDescription', (done) => {
      const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
      pc.setRemoteDescription({type: 'offer', sdp: sdp}, () => {
        expect(pc.remoteDescription.type).to.equal('offer');
        expect(pc.remoteDescription.sdp).to.equal(sdp);
        done();
      });
    });

    describe('when called with an offer containing a track', () => {
      const sdp = SDP_BOILERPLATE +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:1001 msid:stream1 track1\r\n' +
          'a=ssrc:1001 cname:some\r\n';
      it('triggers onaddstream', (done) => {
        pc.onaddstream = function(event) {
          const stream = event.stream;
          expect(stream.getTracks().length).to.equal(1);
          expect(stream.getTracks()[0].kind).to.equal('audio');

          done();
        };
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });

      it('emits a addstream event', (done) => {
        pc.addEventListener('addstream', function(event) {
          const stream = event.stream;
          expect(stream.getTracks().length).to.equal(1);
          expect(stream.getTracks()[0].kind).to.equal('audio');

          done();
        });
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });

      it('triggers ontrack', (done) => {
        pc.ontrack = function(event) {
          expect(event.track.kind).to.equal('audio');
          expect(event.receiver);
          expect(event.streams.length).to.equal(1);

          done();
        };
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });

      it('emits a track event', (done) => {
        pc.addEventListener('track', function(event) {
          expect(event.track.kind).to.equal('audio');
          expect(event.receiver);
          expect(event.streams.length).to.equal(1);

          done();
        });
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });

      it('triggers ontrack and track event before resolving', (done) => {
        let clock = sinon.useFakeTimers();
        var trackEvent = sinon.stub();
        pc.addEventListener('track', trackEvent);
        pc.ontrack = sinon.stub();
        pc.setRemoteDescription({type: 'offer', sdp: sdp})
        .then(() => {
          window.setTimeout(() => {
            expect(trackEvent).to.have.been.calledWith();
            expect(pc.ontrack).to.have.been.calledWith();
            clock.restore();
            done();
          }, 0);
          clock.tick(500);
        });
      });
    });

    describe('when called with an offer without (explicit) tracks', () => {
      const sdp = SDP_BOILERPLATE +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:1001 cname:some\r\n'
          .replace('a=msid-semantics:WMS *\r\n', '');

      it('triggers onaddstream', (done) => {
        pc.onaddstream = function(event) {
          const stream = event.stream;
          expect(stream.getTracks().length).to.equal(1);
          expect(stream.getTracks()[0].kind).to.equal('audio');

          done();
        };
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });

      it('triggers ontrack', (done) => {
        pc.ontrack = function(event) {
          expect(event.track.kind).to.equal('audio');
          expect(event.receiver);
          expect(event.streams.length).to.equal(1);
          done();
        };
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });
    });

    describe('when called with an offer containing multiple streams ' +
        '/ tracks', () => {
      const sdp = SDP_BOILERPLATE +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:1001 msid:stream1 track1\r\n' +
          'a=ssrc:1001 cname:some\r\n' +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\<PASSWORD>' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:2002 msid:stream2 track2\r\n' +
          'a=ssrc:2002 cname:some\r\n';

      it('triggers onaddstream twice', (done) => {
        let numStreams = 0;
        pc.onaddstream = function(event) {
          numStreams++;
          expect(event.stream.id).to.equal('stream' + numStreams);
          if (numStreams === 2) {
            done();
          }
        };
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });

      it('triggers ontrack twice', (done) => {
        let numTracks = 0;
        pc.ontrack = function(event) {
          numTracks++;
          expect(event.streams[0].id).to.equal('stream' + numTracks);
          if (numTracks === 2) {
            done();
          }
        };
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });
    });

    describe('when called with a bundle offer after adding ' +
        'two tracks', () => {
      const sdp = SDP_BOILERPLATE +
          'a=group:BUNDLE audio1 video1\r\n' +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:1001 msid:stream1 track1\r\n' +
          'a=ssrc:1001 cname:some\r\n' +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:2002 msid:stream2 track2\r\n' +
          'a=ssrc:2002 cname:some\r\n';
      it('disposes the second ice transport', (done) => {
        navigator.mediaDevices.getUserMedia({audio: true, video: true})
        .then((stream) => {
          // this creates two transceivers with ice transports.
          pc.addStream(stream);

          // this has bundle so will set usingBundle. But two
          // transceivers and their ice/dtls transports exist
          // and the second one needs to be disposed.
          return pc.setRemoteDescription({type: 'offer', sdp: sdp});
        })
        .then(() => {
          // the second ice transport should have been disposed.
          expect(pc.transceivers[0].iceTransport)
              .to.equal(pc.transceivers[1].iceTransport);
          done();
        });
      });
    });

    // TODO: add a test for recvonly to show it doesn't trigger the callback.
    //   probably easiest done using a sinon.stub
    //
    describe('sets the canTrickleIceCandidates property', () => {
      it('to true when called with an offer that contains ' +
          'a=ice-options:trickle', (done) => {
        const sdp = SDP_BOILERPLATE +<commit_msg>test: dont use internal private transceivers API<commit_after>/*
 *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
const chai = require('chai');
const expect = chai.expect;
const sinon = require('sinon');
chai.use(require('dirty-chai'));
chai.use(require('sinon-chai'));

const mockORTC = require('./ortcmock');
const mockGetUserMedia = require('./gummock');
const shimPeerConnection = require('../rtcpeerconnection');
const SDPUtils = require('sdp');

const FINGERPRINT_SHA256 = '8C:71:B3:8D:A5:38:FD:8F:A4:2E:A2:65:6C' +
    ':86:52:BC:E0:6E:94:F2:9F:7C:4D:B5:DF:AF:AA:6F:44:90:8D:F4';
const ICEUFRAG = 'someufrag';
const ICEPWD = '<PASSWORD>';
const SDP_BOILERPLATE = 'v=0\r\n' +
    'o=- 166855176514521964 2 IN IP4 127.0.0.1\r\n' +
    's=-\r\n' +
    't=0 0\r\n' +
    'a=msid-semantic:WMS *\r\n';
const MINIMAL_AUDIO_MLINE =
    'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
    'c=IN IP4 0.0.0.0\r\n' +
    'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
    'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
    'a=ice-pwd:' + ICEPWD + '\r\n' +
    'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
    'a=setup:actpass\r\n' +
    'a=mid:audio1\r\n' +
    'a=sendonly\r\n' +
    'a=rtcp-mux\r\n' +
    'a=rtcp-rsize\r\n' +
    'a=rtpmap:111 opus/48000/2\r\n' +
    'a=ssrc:1001 cname:some\r\n';


// this detects that we are not running in a browser.
const mockWindow = typeof window === 'undefined';

describe('Edge shim', () => {
  let RTCPeerConnection;
  beforeEach(() => {
    if (mockWindow) {
      global.window = {setTimeout};
      mockGetUserMedia(window);
      mockORTC(window);
      global.navigator = window.navigator;
    }
    RTCPeerConnection = shimPeerConnection(window, 15025);
  });

  beforeEach(() => {
    let streams = [];
    let release = () => {
      streams.forEach((stream) => {
        stream.getTracks().forEach((track) => {
          track.stop();
        });
      });
      streams = [];
    };

    let origGetUserMedia = navigator.getUserMedia.bind(navigator);
    navigator.getUserMedia = (constraints, cb, eb) => {
      origGetUserMedia(constraints, (stream) => {
        streams.push(stream);
        if (cb) {
          cb.apply(null, [stream]);
        }
      }, eb);
    };
    navigator.getUserMedia.restore = () => {
      navigator.getUserMedia = origGetUserMedia;
      release();
    };

    let origMediaDevicesGetUserMedia =
        navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
    navigator.mediaDevices.getUserMedia = (constraints) => {
      return origMediaDevicesGetUserMedia(constraints, (stream) => {
        streams.push(stream);
        return stream;
      });
    };
    navigator.mediaDevices.getUserMedia.restore = () => {
      navigator.mediaDevices.getUserMedia = origMediaDevicesGetUserMedia;
      release();
    };
  });

  afterEach(() => {
    navigator.getUserMedia.restore();
    navigator.mediaDevices.getUserMedia.restore();
  });

  describe('RTCPeerConnection constructor', () => {
    it('throws a NotSupportedError when called with ' +
        'rtcpMuxPolicy negotiate', () => {
      const constructor = () => {
        return new RTCPeerConnection({rtcpMuxPolicy: 'negotiate'});
      };
      expect(constructor).to.throw(/rtcpMuxPolicy/)
          .that.has.property('name').that.equals('NotSupportedError');
    });

    describe('when RTCIceCandidatePoolSize is set', () => {
      beforeEach(() => {
        sinon.spy(window, 'RTCIceGatherer');
      });

      afterEach(() => {
        window.RTCIceGatherer.restore();
      });

      it('creates an ICE Gatherer', () => {
        new RTCPeerConnection({iceCandidatePoolSize: 1});
        expect(window.RTCIceGatherer).to.have.been.calledOnce();
      });

      // TODO: those tests are convenient because they are sync and
      //    dont require createOffer-SLD before creating the gatherer.
      it('sets default ICETransportPolicy on RTCIceGatherer', () => {
        new RTCPeerConnection({iceCandidatePoolSize: 1});
        expect(window.RTCIceGatherer).to.have.been.calledWith(sinon.match({
          gatherPolicy: 'all'
        }));
      });

      it('sets ICETransportPolicy=all on RTCIceGatherer', () => {
        new RTCPeerConnection({iceCandidatePoolSize: 1,
            iceTransportPolicy: 'all'});
        expect(window.RTCIceGatherer).to.have.been.calledWith(sinon.match({
          gatherPolicy: 'all'
        }));
      });
      it('sets ICETransportPolicy=relay on RTCIceGatherer', () => {
        new RTCPeerConnection({iceCandidatePoolSize: 1,
            iceTransportPolicy: 'relay'});
        expect(window.RTCIceGatherer).to.have.been.calledWith(sinon.match({
          gatherPolicy: 'relay'
        }));
      });
    });
  });

  describe('setLocalDescription', () => {
    let pc;
    beforeEach(() => {
      pc = new RTCPeerConnection();
    });
    afterEach(() => {
      pc.close();
    });

    it('returns a promise', (done) => {
      pc.createOffer({offerToReceiveAudio: 1})
      .then((offer) => {
        return pc.setLocalDescription(offer);
      })
      .then(done);
    });

    it('calls the legacy success callback', (done) => {
      pc.createOffer({offerToReceiveAudio: 1})
      .then((offer) => {
        return pc.setLocalDescription(offer, done, () => {});
      });
    });

    it('changes the signalingState to have-local-offer', (done) => {
      pc.createOffer({offerToReceiveAudio: 1})
      .then((offer) => {
        return pc.setLocalDescription(offer);
      })
      .then(() => {
        expect(pc.localDescription.type).to.equal('offer');
        expect(pc.signalingState = 'have-local-offer');
        done();
      });
    });

    describe('InvalidStateError is thrown when called with', () => {
      const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
      it('an offer in signalingState have-remote-offer', (done) => {
        pc.setRemoteDescription({type: 'offer', sdp})
        .then(() => {
          return pc.setLocalDescription({type: 'offer'});
        })
        .catch((e) => {
          expect(e.name).to.equal('InvalidStateError');
          done();
        });
      });

      it('an answer in signalingState have-local-offer', (done) => {
        pc.createOffer({offerToReceiveAudio: 1})
        .then((offer) => {
          return pc.setLocalDescription(offer);
        })
        .then(() => {
          return pc.setLocalDescription({type: 'answer'});
        })
        .catch((e) => {
          expect(e.name).to.equal('InvalidStateError');
          done();
        });
      });
    });

    describe('starts emitting ICE candidates', () => {
      let clock;
      beforeEach(() => {
        clock = sinon.useFakeTimers();
      });
      afterEach(() => {
        clock.restore();
      });

      describe('calls', () => {
        it('the onicecandidate callback', (done) => {
          pc.onicegatheringstatechange = () => {
            if (pc.iceGatheringState === 'complete') {
              expect(pc.onicecandidate).to.have.been.calledWith();
              done();
            }
          };
          pc.onicecandidate = sinon.stub();
          pc.createOffer({offerToReceiveAudio: 1})
          .then(offer => pc.setLocalDescription(offer))
          .then(() => {
            window.setTimeout(() => {
              clock.tick(500);
            });
            clock.tick(0);
          });
        });
        it('the icecandidate event listener', (done) => {
          const stub = sinon.stub();
          pc.onicegatheringstatechange = () => {
            if (pc.iceGatheringState === 'complete') {
              expect(stub).to.have.been.calledWith();
              done();
            }
          };
          pc.addEventListener('icecandidate', stub);
          pc.createOffer({offerToReceiveAudio: 1})
          .then(offer => pc.setLocalDescription(offer))
          .then(() => {
            window.setTimeout(() => {
              clock.tick(500);
            });
            clock.tick(0);
          });
        });
      });

      it('updates localDescription.sdp with candidates', (done) => {
        pc.onicegatheringstatechange = () => {
          if (pc.iceGatheringState === 'complete') {
            expect(SDPUtils.matchPrefix(pc.localDescription.sdp,
                'a=candidate:').length).to.be.above(0);
            expect(SDPUtils.matchPrefix(pc.localDescription.sdp,
                'a=end-of-candidates')).to.have.length(1);
            done();
          }
        };
        pc.createOffer({offerToReceiveAudio: 1})
        .then(offer => pc.setLocalDescription(offer))
        .then(() => {
          window.setTimeout(() => {
            clock.tick(500);
          });
          clock.tick(0);
        });
      });

      it('changes iceGatheringState and emits icegatheringstatechange ' +
          'event', (done) => {
        let states = [];
        pc.addEventListener('icegatheringstatechange', () => {
          states.push(pc.iceGatheringState);
          if (pc.iceGatheringState === 'complete') {
            expect(states.length).to.equal(2);
            expect(states).to.contain('gathering');
            expect(states).to.contain('complete');
            done();
          }
        });
        pc.createOffer({offerToReceiveAudio: 1})
        .then(offer => pc.setLocalDescription(offer))
        .then(() => {
          expect(pc.iceGatheringState).to.equal('new');
          clock.tick(500);
        });
      });
    });

    describe('after setRemoteDescription', () => {
      beforeEach(() => {
        sinon.spy(window.RTCIceTransport.prototype, 'start');
        sinon.spy(window.RTCDtlsTransport.prototype, 'start');
      });
      afterEach(() => {
        window.RTCIceTransport.prototype.start.restore();
        window.RTCDtlsTransport.prototype.start.restore();
      });

      const sdp = SDP_BOILERPLATE +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:1001 cname:some\r\n';
      it('starts the ice transport', (done) => {
        pc.setRemoteDescription({type: 'offer', sdp: sdp})
        .then(() => {
          return pc.createAnswer();
        })
        .then((answer) => {
          return pc.setLocalDescription(answer);
        })
        .then(() => {
          const receiver = pc.getReceivers()[0];
          const iceTransport = receiver.transport.transport;
          expect(iceTransport.start).to.have.been.calledOnce();
          expect(iceTransport.start).to.have.been.calledWith(
            sinon.match.any,
            sinon.match({
              usernameFragment: '' + ICEUFRAG + '',
              password: '' + ICE<PASSWORD> + ''
            })
          );
          done();
        });
      });

      it('starts the dtls transport', (done) => {
        pc.setRemoteDescription({type: 'offer', sdp: sdp})
        .then(() => {
          return pc.createAnswer();
        })
        .then((answer) => {
          return pc.setLocalDescription(answer);
        })
        .then(() => {
          const receiver = pc.getReceivers()[0];
          const dtlsTransport = receiver.transport;
          expect(dtlsTransport.start).to.have.been.calledOnce();
          expect(dtlsTransport.start).to.have.been.calledWith(
            sinon.match({
              role: 'auto',
              fingerprints: sinon.match([
                sinon.match({
                  algorithm: 'sha-256',
                  value: FINGERPRINT_SHA256
                })
              ])
            })
          );
          done();
        });
      });
    });
  });

  describe('setRemoteDescription', () => {
    let pc;
    beforeEach(() => {
      pc = new RTCPeerConnection();
    });
    afterEach(() => {
      pc.close();
    });

    it('returns a promise', (done) => {
      const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
      pc.setRemoteDescription({type: 'offer', sdp: sdp})
      .then(done);
    });
    it('calls the legacy success callback', (done) => {
      const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
      pc.setRemoteDescription({type: 'offer', sdp: sdp}, done, () => {});
    });

    it('changes the signalingState to have-remote-offer', (done) => {
      const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
      pc.setRemoteDescription({type: 'offer', sdp: sdp})
      .then(() => {
        expect(pc.signalingState = 'have-remote-offer');
        done();
      });
    });

    it('sets the remoteDescription', (done) => {
      const sdp = SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE;
      pc.setRemoteDescription({type: 'offer', sdp: sdp}, () => {
        expect(pc.remoteDescription.type).to.equal('offer');
        expect(pc.remoteDescription.sdp).to.equal(sdp);
        done();
      });
    });

    describe('when called with an offer containing a track', () => {
      const sdp = SDP_BOILERPLATE +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:1001 msid:stream1 track1\r\n' +
          'a=ssrc:1001 cname:some\r\n';
      it('triggers onaddstream', (done) => {
        pc.onaddstream = function(event) {
          const stream = event.stream;
          expect(stream.getTracks().length).to.equal(1);
          expect(stream.getTracks()[0].kind).to.equal('audio');

          done();
        };
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });

      it('emits a addstream event', (done) => {
        pc.addEventListener('addstream', function(event) {
          const stream = event.stream;
          expect(stream.getTracks().length).to.equal(1);
          expect(stream.getTracks()[0].kind).to.equal('audio');

          done();
        });
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });

      it('triggers ontrack', (done) => {
        pc.ontrack = function(event) {
          expect(event.track.kind).to.equal('audio');
          expect(event.receiver);
          expect(event.streams.length).to.equal(1);

          done();
        };
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });

      it('emits a track event', (done) => {
        pc.addEventListener('track', function(event) {
          expect(event.track.kind).to.equal('audio');
          expect(event.receiver);
          expect(event.streams.length).to.equal(1);

          done();
        });
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });

      it('triggers ontrack and track event before resolving', (done) => {
        let clock = sinon.useFakeTimers();
        var trackEvent = sinon.stub();
        pc.addEventListener('track', trackEvent);
        pc.ontrack = sinon.stub();
        pc.setRemoteDescription({type: 'offer', sdp: sdp})
        .then(() => {
          window.setTimeout(() => {
            expect(trackEvent).to.have.been.calledWith();
            expect(pc.ontrack).to.have.been.calledWith();
            clock.restore();
            done();
          }, 0);
          clock.tick(500);
        });
      });
    });

    describe('when called with an offer without (explicit) tracks', () => {
      const sdp = SDP_BOILERPLATE +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:1001 cname:some\r\n'
          .replace('a=msid-semantics:WMS *\r\n', '');

      it('triggers onaddstream', (done) => {
        pc.onaddstream = function(event) {
          const stream = event.stream;
          expect(stream.getTracks().length).to.equal(1);
          expect(stream.getTracks()[0].kind).to.equal('audio');

          done();
        };
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });

      it('triggers ontrack', (done) => {
        pc.ontrack = function(event) {
          expect(event.track.kind).to.equal('audio');
          expect(event.receiver);
          expect(event.streams.length).to.equal(1);
          done();
        };
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });
    });

    describe('when called with an offer containing multiple streams ' +
        '/ tracks', () => {
      const sdp = SDP_BOILERPLATE +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:1001 msid:stream1 track1\r\n' +
          'a=ssrc:1001 cname:some\r\n' +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:2002 msid:stream2 track2\r\n' +
          'a=ssrc:2002 cname:some\r\n';

      it('triggers onaddstream twice', (done) => {
        let numStreams = 0;
        pc.onaddstream = function(event) {
          numStreams++;
          expect(event.stream.id).to.equal('stream' + numStreams);
          if (numStreams === 2) {
            done();
          }
        };
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });

      it('triggers ontrack twice', (done) => {
        let numTracks = 0;
        pc.ontrack = function(event) {
          numTracks++;
          expect(event.streams[0].id).to.equal('stream' + numTracks);
          if (numTracks === 2) {
            done();
          }
        };
        pc.setRemoteDescription({type: 'offer', sdp: sdp});
      });
    });

    describe('when called with a bundle offer after adding ' +
        'two tracks', () => {
      const sdp = SDP_BOILERPLATE +
          'a=group:BUNDLE audio1 video1\r\n' +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:1001 msid:stream1 track1\r\n' +
          'a=ssrc:1001 cname:some\r\n' +
          'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
          'c=IN IP4 0.0.0.0\r\n' +
          'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
          'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
          'a=ice-pwd:' + ICEPWD + '\r\n' +
          'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
          'a=setup:actpass\r\n' +
          'a=mid:audio1\r\n' +
          'a=sendonly\r\n' +
          'a=rtcp-mux\r\n' +
          'a=rtcp-rsize\r\n' +
          'a=rtpmap:111 opus/48000/2\r\n' +
          'a=ssrc:2002 msid:stream2 track2\r\n' +
          'a=ssrc:2002 cname:some\r\n';
      it('disposes the second ice transport', (done) => {
        navigator.mediaDevices.getUserMedia({audio: true, video: true})
        .then((stream) => {
          // this creates two transceivers with ice transports.
          pc.addStream(stream);

          // this has bundle so will set usingBundle. But two
          // transceivers and their ice/dtls transports exist
          // and the second one needs to be disposed.
          return pc.setRemoteDescription({type: 'offer', sdp: sdp});
        })
        .then(() => {
          const senders = pc.getSenders();
          // the second ice transport should have been disposed.
          expect(senders[0].transport.transport).to
              .equal(senders[1].transport.transport);
          done();
        });
      });
    });

    // TODO: add a test for recvonly to show it doesn't trigger the callback.
    //   probably easiest done using a sinon.stub
    //
    describe('sets the canTrickleIceCandidates property', () => {
      it('to true when called with an offer that contains ' +
          'a=ice-options:trickle', (done) => {
        const sdp = SDP_BOILERPLATE +
{'fraction_non_alphanumeric': 0.17120863970588235, 'fraction_numerical': 0.020703125, 'mean_word_length': 1.975794871794872, 'pattern_counts': {'":': 0, '<': 8, '<?xml version=': 0, '>': 220, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 34, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3276745', 'n_tokens_mistral': 15551, 'n_tokens_neox': 14747, 'n_words': 2305}