Skip to content

Commit

Permalink
Fix ACK re-INVITE race handling - update tests
Browse files Browse the repository at this point in the history
  • Loading branch information
John Riordan committed May 1, 2020
1 parent 2266a38 commit e6b0b4b
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 5 deletions.
208 changes: 205 additions & 3 deletions test/spec/api/session-in-dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const SIP_404 = [jasmine.stringMatching(/^SIP\/2.0 404/)];
const SIP_407 = [jasmine.stringMatching(/^SIP\/2.0 407/)];
const SIP_481 = [jasmine.stringMatching(/^SIP\/2.0 481/)];
const SIP_488 = [jasmine.stringMatching(/^SIP\/2.0 488/)];
const SIP_500 = [jasmine.stringMatching(/^SIP\/2.0 500/)];
const SIP_RETRY_AFTER = [jasmine.stringMatching(/Retry-After: /)];

//
// Simulatineous SIP Request Processing
Expand Down Expand Up @@ -591,14 +593,214 @@ describe("API Session In-Dialog", () => {
await soon();
});

describe("Alice invite()", () => {
beforeEach(() => {
describe("Alice invite() without sdp", () => {
beforeEach(async () => {
inviter.invite({ withoutSdp: true });
await bob.transport.waitSent();
});

describe("Bob accept() ACK dropped", () => {
beforeEach(async () => {
resetSpies();
bob.transport.receiveDropOnce(); // ACK
invitation.accept();
await alice.transport.waitSent(); // ACK
});

it("her session state should be `established`", () => {
expect(inviter.state).toBe(SessionState.Established);
});

it("his session state should be `established`", () => {
expect(invitation.state).toBe(SessionState.Established);
});

describe("Re-INVITE race", () => {
beforeEach(async () => {
resetSpies();
invitation.delegate = undefined;
inviter.invite();
await bob.transport.waitSent(); // 500
});

it("her ua should send INVITE, ACK", () => {
const spy = alice.transportSendSpy;
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.calls.argsFor(0)).toEqual(SIP_INVITE);
expect(spy.calls.argsFor(1)).toEqual(SIP_ACK);
});

it("her ua should receive 500 with Retry-After", () => {
const spy = alice.transportReceiveSpy;
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.calls.argsFor(0)).toEqual(SIP_500);
expect(spy.calls.argsFor(0)).toEqual(SIP_RETRY_AFTER);
});

it("her signaling should be stable", () => {
if (!inviter.dialog) {
fail("Session dialog undefined");
return;
}
expect(inviter.dialog.signalingState).toEqual(SignalingState.Stable);
});

it("his signaling should be have-local-offer", () => {
if (!invitation.dialog) {
fail("Session dialog undefined");
return;
}
expect(invitation.dialog.signalingState).toEqual(SignalingState.HaveLocalOffer);
});

it("her session state should be `established`", () => {
expect(inviter.state).toBe(SessionState.Established);
});

it("his session state should be `established`", () => {
expect(invitation.state).toBe(SessionState.Established);
});
});
});

describe("Bob accept() ACK processing", () => {
beforeEach(async () => {
resetSpies();
invitation.accept();
(invitation.sessionDescriptionHandler as any).setDescriptionWaitOnce = true;
await alice.transport.waitSent(); // ACK
});

it("her session state should be `established`", () => {
expect(inviter.state).toBe(SessionState.Established);
});

it("his session state should be `established`", () => {
expect(invitation.state).toBe(SessionState.Established);
});

describe("Re-INVITE race", () => {
beforeEach(async () => {
resetSpies();
invitation.delegate = undefined;
inviter.invite();
await bob.transport.waitSent();
await soon(1);
});

it("her ua should send INVITE, ACK", () => {
const spy = alice.transportSendSpy;
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.calls.argsFor(0)).toEqual(SIP_INVITE);
expect(spy.calls.argsFor(1)).toEqual(SIP_ACK);
});

it("her ua should receive 500 with Retry-After", () => {
const spy = alice.transportReceiveSpy;
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.calls.argsFor(0)).toEqual(SIP_500);
expect(spy.calls.argsFor(0)).toEqual(SIP_RETRY_AFTER);
});

it("her signaling should be stable", () => {
if (!inviter.dialog) {
fail("Session dialog undefined");
return;
}
expect(inviter.dialog.signalingState).toEqual(SignalingState.Stable);
});

it("his signaling should be stable", () => {
if (!invitation.dialog) {
fail("Session dialog undefined");
return;
}
expect(invitation.dialog.signalingState).toEqual(SignalingState.Stable);
});

it("her session state should be `established`", () => {
expect(inviter.state).toBe(SessionState.Established);
});

it("his session state should be `established`", () => {
expect(invitation.state).toBe(SessionState.Established);
});
});
});
});

describe("Alice invite() with sdp", () => {
beforeEach(async () => {
return inviter.invite()
.then(() => bob.transport.waitSent());
});

describe("Bob accept() ACK dropped", () => {
beforeEach(async () => {
resetSpies();
bob.transport.receiveDropOnce(); // ACK
invitation.accept();
await alice.transport.waitSent(); // ACK
});

it("her session state should be `established`", () => {
expect(inviter.state).toBe(SessionState.Established);
});

it("his session state should be `established`", () => {
expect(invitation.state).toBe(SessionState.Established);
});

describe("Re-INVITE race", () => {
beforeEach(async () => {
resetSpies();
invitation.delegate = undefined;
inviter.invite();
await bob.transport.waitSent();
await alice.transport.waitSent();
});

it("her ua should send INVITE, ACK", () => {
const spy = alice.transportSendSpy;
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.calls.argsFor(0)).toEqual(SIP_INVITE);
expect(spy.calls.argsFor(1)).toEqual(SIP_ACK);
});

it("her ua should receive 200", () => {
const spy = alice.transportReceiveSpy;
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.calls.argsFor(0)).toEqual(SIP_200);
});

it("her signaling should be stable", () => {
if (!inviter.dialog) {
fail("Session dialog undefined");
return;
}
expect(inviter.dialog.signalingState).toEqual(SignalingState.Stable);
});

it("his signaling should be stable", () => {
if (!invitation.dialog) {
fail("Session dialog undefined");
return;
}
expect(invitation.dialog.signalingState).toEqual(SignalingState.Stable);
});

it("her session state should be `established`", () => {
expect(inviter.state).toBe(SessionState.Established);
});

it("his session state should be `established`", () => {
expect(invitation.state).toBe(SessionState.Established);
});
});
});

describe("Bob accept()", () => {
beforeEach(() => {
beforeEach(async () => {
resetSpies();
return invitation.accept()
.then(() => alice.transport.waitSent()); // ACK
Expand Down
19 changes: 18 additions & 1 deletion test/support/api/session-description-handler-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function makeMockSessionDescriptionHandler(name: string, id: number): jas
obj.getDescriptionRejectOnce = undefined;
obj.getDescriptionUndefinedBodyOnce = undefined;
obj.setDescriptionRejectOnce = undefined;
obj.setDescriptionWaitOnce = undefined;

sdh.close.and.callFake(() => {
// console.warn(`SDH.close[${name}][${id}]`);
Expand Down Expand Up @@ -109,7 +110,7 @@ export function makeMockSessionDescriptionHandler(name: string, id: number): jas
});
});

sdh.setDescription.and.callFake(() => {
sdh.setDescription.and.callFake((sdp: string) => {
if (closed) {
throw new Error(`SDH.setDescription[${name}][${id}] SDH closed`);
}
Expand All @@ -119,12 +120,28 @@ export function makeMockSessionDescriptionHandler(name: string, id: number): jas
return Promise.reject(new Error(`SDH.setDescription[${name}][${id}] SDH test failure`));
}

if ((sdh as any).setDescriptionWaitOnce) { // hacky
const timeout = 1;
(sdh as any).setDescriptionWaitOnce = undefined;
return new Promise((resolve, reject) => {
setTimeout(() => {
(sdh as any).setDescription().then(() => resolve());
}, timeout);
});
}

const fromState = state;
switch (state) {
case "stable":
if (sdp === "SDP ANSWER") {
throw new Error(`SDH.setDescription[${name}][${id}] ${fromState} => ${state} Invalid SDH state transition - expected offer`);
}
state = "has-remote-offer";
break;
case "has-local-offer":
if (sdp === "SDP OFFER") {
throw new Error(`SDH.setDescription[${name}][${id}] ${fromState} => ${state} Invalid SDH state transition - expected answer`);
}
state = "stable";
break;
case "has-remote-offer":
Expand Down
10 changes: 9 additions & 1 deletion test/support/api/transport-fake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class TransportFake extends EventEmitter implements Transport {
private waitingForReceiveResolve: ResolveFunction | undefined;
private waitingForReceiveReject: RejectFunction | undefined;

private _receiveDropOnce = false;
private _state: TransportState = TransportState.Disconnected;
private _stateEventEmitter = new EventEmitter();

Expand Down Expand Up @@ -83,12 +84,19 @@ export class TransportFake extends EventEmitter implements Transport {
message += `Receiving...\n${msg}`;
// this.logger.log(message);
this.emit("message", msg);
if (this.onMessage) {
if (this._receiveDropOnce) {
this._receiveDropOnce = false;
this.logger.warn((this._id ? `${this._id} ` : "") + "Dropped message");
} else if (this.onMessage) {
this.onMessage(msg);
}
this.receiveHappened();
}

public receiveDropOnce(): void {
this._receiveDropOnce = true;
}

public async waitSent(): Promise<void> {
if (this.waitingForSendPromise) {
throw new Error("Already waiting for send.");
Expand Down
4 changes: 4 additions & 0 deletions test/support/core/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "../../../src/api";
import {
DigestAuthentication,
IncomingAckRequest,
IncomingInviteRequest,
IncomingRequestMessage,
IncomingResponseMessage,
Expand Down Expand Up @@ -143,6 +144,9 @@ export function makeMockSessionDelegate(): jasmine.SpyObj<Required<SessionDelega
"onPrack",
"onRefer"
]);
delegate.onAck.and.callFake((request: IncomingAckRequest) => {
return Promise.resolve();
});
return delegate;
}

Expand Down

0 comments on commit e6b0b4b

Please sign in to comment.