mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-09 15:08:12 -05:00
596 lines
16 KiB
JavaScript
596 lines
16 KiB
JavaScript
const isNodejs = typeof window === "undefined";
|
|
|
|
if (isNodejs) {
|
|
// make the tests runnable in both the browser and Node.js
|
|
await import("./node-imports.js");
|
|
}
|
|
|
|
const { expect } = chai;
|
|
|
|
const URL = "http://localhost:3000";
|
|
const WS_URL = URL.replace("http", "ws");
|
|
|
|
const PING_INTERVAL = 300;
|
|
const PING_TIMEOUT = 200;
|
|
|
|
function sleep(delay) {
|
|
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
}
|
|
|
|
function waitFor(socket, eventType) {
|
|
return new Promise((resolve) => {
|
|
socket.addEventListener(
|
|
eventType,
|
|
(event) => {
|
|
resolve(event);
|
|
},
|
|
{ once: true }
|
|
);
|
|
});
|
|
}
|
|
|
|
function decodePayload(payload) {
|
|
const firstColonIndex = payload.indexOf(":");
|
|
const length = payload.substring(0, firstColonIndex);
|
|
const packet = payload.substring(firstColonIndex + 1);
|
|
return [length, packet];
|
|
}
|
|
|
|
async function initLongPollingSession(supportsBinary = false) {
|
|
const response = await fetch(`${URL}/engine.io/?EIO=3&transport=polling` + (supportsBinary ? "" : "&b64=1"));
|
|
const text = await response.text();
|
|
const [, content] = decodePayload(text);
|
|
return JSON.parse(content.substring(1)).sid;
|
|
}
|
|
|
|
describe("Engine.IO protocol", () => {
|
|
describe("handshake", () => {
|
|
describe("HTTP long-polling", () => {
|
|
it("successfully opens a session", async () => {
|
|
const response = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling`
|
|
);
|
|
|
|
expect(response.status).to.eql(200);
|
|
|
|
const text = await response.text();
|
|
const [length, content] = decodePayload(text);
|
|
|
|
expect(length).to.eql(content.length.toString());
|
|
expect(content).to.startsWith("0");
|
|
|
|
const value = JSON.parse(content.substring(1));
|
|
|
|
expect(value.sid).to.be.a("string");
|
|
expect(value.upgrades).to.eql(["websocket"]);
|
|
expect(value.pingInterval).to.eql(PING_INTERVAL);
|
|
expect(value.pingTimeout).to.eql(PING_TIMEOUT);
|
|
expect(value.maxPayload).to.be.oneOf([undefined, 1000000]);
|
|
});
|
|
|
|
it("fails with an invalid 'transport' query parameter", async () => {
|
|
const response = await fetch(`${URL}/engine.io/?EIO=3`);
|
|
|
|
expect(response.status).to.eql(400);
|
|
|
|
const response2 = await fetch(`${URL}/engine.io/?EIO=3&transport=abc`);
|
|
|
|
expect(response2.status).to.eql(400);
|
|
});
|
|
|
|
it("fails with an invalid request method", async () => {
|
|
const response = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling`,
|
|
{
|
|
method: "post",
|
|
}
|
|
);
|
|
|
|
expect(response.status).to.eql(400);
|
|
});
|
|
});
|
|
|
|
describe("WebSocket", () => {
|
|
it("successfully opens a session", async () => {
|
|
const socket = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
|
);
|
|
|
|
const { data } = await waitFor(socket, "message");
|
|
|
|
expect(data).to.startsWith("0");
|
|
|
|
const value = JSON.parse(data.substring(1));
|
|
|
|
expect(value.sid).to.be.a("string");
|
|
expect(value.upgrades).to.eql([]);
|
|
expect(value.pingInterval).to.eql(PING_INTERVAL);
|
|
expect(value.pingTimeout).to.eql(PING_TIMEOUT);
|
|
expect(value.maxPayload).to.be.oneOf([undefined, 1000000]);
|
|
|
|
socket.close();
|
|
});
|
|
|
|
it("fails with an invalid 'EIO' query parameter", async () => {
|
|
const socket = new WebSocket(
|
|
`${WS_URL}/engine.io/?transport=websocket`
|
|
);
|
|
|
|
if (isNodejs) {
|
|
socket.on("error", () => {});
|
|
}
|
|
|
|
waitFor(socket, "close");
|
|
|
|
const socket2 = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=abc&transport=websocket`
|
|
);
|
|
|
|
if (isNodejs) {
|
|
socket2.on("error", () => {});
|
|
}
|
|
|
|
waitFor(socket2, "close");
|
|
});
|
|
|
|
it("fails with an invalid 'transport' query parameter", async () => {
|
|
const socket = new WebSocket(`${WS_URL}/engine.io/?EIO=3`);
|
|
|
|
if (isNodejs) {
|
|
socket.on("error", () => {});
|
|
}
|
|
|
|
waitFor(socket, "close");
|
|
|
|
const socket2 = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=3&transport=abc`
|
|
);
|
|
|
|
if (isNodejs) {
|
|
socket2.on("error", () => {});
|
|
}
|
|
|
|
waitFor(socket2, "close");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("message", () => {
|
|
describe("HTTP long-polling", () => {
|
|
it("sends and receives a payload containing one plain text packet", async () => {
|
|
const sid = await initLongPollingSession();
|
|
|
|
const pushResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
|
{
|
|
method: "post",
|
|
body: "6:4hello",
|
|
}
|
|
);
|
|
|
|
expect(pushResponse.status).to.eql(200);
|
|
|
|
const postContent = await pushResponse.text();
|
|
|
|
expect(postContent).to.eql("ok");
|
|
|
|
const pollResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
|
);
|
|
|
|
expect(pollResponse.status).to.eql(200);
|
|
|
|
const pollContent = await pollResponse.text();
|
|
|
|
expect(pollContent).to.eql("6:4hello");
|
|
});
|
|
|
|
it("sends and receives a payload containing several plain text packets", async () => {
|
|
const sid = await initLongPollingSession();
|
|
|
|
const pushResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
|
{
|
|
method: "post",
|
|
body: "6:4test16:4test26:4test3",
|
|
}
|
|
);
|
|
|
|
expect(pushResponse.status).to.eql(200);
|
|
|
|
const postContent = await pushResponse.text();
|
|
|
|
expect(postContent).to.eql("ok");
|
|
|
|
const pollResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
|
);
|
|
|
|
expect(pollResponse.status).to.eql(200);
|
|
|
|
const pollContent = await pollResponse.text();
|
|
|
|
expect(pollContent).to.eql("6:4test16:4test26:4test3");
|
|
});
|
|
|
|
it("sends and receives a payload containing plain text and binary packets (base64 encoded)", async () => {
|
|
const sid = await initLongPollingSession();
|
|
|
|
const pushResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
|
{
|
|
method: "post",
|
|
body: "6:4hello10:b4AQIDBA==",
|
|
}
|
|
);
|
|
|
|
expect(pushResponse.status).to.eql(200);
|
|
|
|
const postContent = await pushResponse.text();
|
|
|
|
expect(postContent).to.eql("ok");
|
|
|
|
const pollResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
|
);
|
|
|
|
expect(pollResponse.status).to.eql(200);
|
|
|
|
const pollContent = await pollResponse.text();
|
|
|
|
expect(pollContent).to.eql("6:4hello10:b4AQIDBA==");
|
|
});
|
|
|
|
it("sends and receives a payload containing plain text and binary packets (binary)", async () => {
|
|
const sid = await initLongPollingSession(true);
|
|
|
|
const pushResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
|
{
|
|
method: "post",
|
|
body: "6:4hello10:b4AQIDBA==",
|
|
}
|
|
);
|
|
|
|
expect(pushResponse.status).to.eql(200);
|
|
|
|
const postContent = await pushResponse.text();
|
|
|
|
expect(postContent).to.eql("ok");
|
|
|
|
const pollResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
|
);
|
|
|
|
expect(pollResponse.status).to.eql(200);
|
|
|
|
const buffer = await pollResponse.arrayBuffer();
|
|
|
|
// 0 => string
|
|
// 6 => byte length
|
|
// 255 => delimiter
|
|
// 52 => 4 (MESSAGE packet type)
|
|
// 104 101 108 108 111 => "hello"
|
|
// 1 => binary
|
|
// 5 => byte length
|
|
// 255 => delimiter
|
|
// 4 => 4 (MESSAGE packet type)
|
|
// 1 2 3 4 => binary message
|
|
expect(buffer).to.eql(Uint8Array.from([0, 6, 255, 52, 104, 101, 108, 108, 111, 1, 5, 255, 4, 1, 2, 3, 4]).buffer);
|
|
});
|
|
|
|
it("closes the session upon invalid packet format", async () => {
|
|
const sid = await initLongPollingSession();
|
|
|
|
try {
|
|
const pushResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
|
{
|
|
method: "post",
|
|
body: "abc",
|
|
}
|
|
);
|
|
|
|
expect(pushResponse.status).to.eql(400);
|
|
} catch (e) {
|
|
// node-fetch throws when the request is closed abnormally
|
|
}
|
|
|
|
const pollResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
|
);
|
|
|
|
expect(pollResponse.status).to.eql(400);
|
|
});
|
|
|
|
// FIXME CORS error
|
|
it.skip("closes the session upon duplicate poll requests", async () => {
|
|
const sid = await initLongPollingSession();
|
|
|
|
const pollResponses = await Promise.all([
|
|
fetch(`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`),
|
|
sleep(5).then(() => fetch(`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}&t=burst`)),
|
|
]);
|
|
|
|
expect(pollResponses[0].status).to.eql(200);
|
|
|
|
const content = await pollResponses[0].text();
|
|
|
|
expect(content).to.eql("1:1");
|
|
|
|
// the Node.js implementation uses HTTP 500 (Internal Server Error), but HTTP 400 seems more suitable
|
|
expect(pollResponses[1].status).to.be.oneOf([400, 500]);
|
|
|
|
const pollResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
|
);
|
|
|
|
expect(pollResponse.status).to.eql(500);
|
|
});
|
|
});
|
|
|
|
describe("WebSocket", () => {
|
|
it("sends and receives a plain text packet", async () => {
|
|
const socket = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
|
);
|
|
|
|
await waitFor(socket, "open");
|
|
|
|
await waitFor(socket, "message"); // handshake
|
|
|
|
socket.send("4hello");
|
|
|
|
const { data } = await waitFor(socket, "message");
|
|
|
|
expect(data).to.eql("4hello");
|
|
|
|
socket.close();
|
|
});
|
|
|
|
it("sends and receives a binary packet", async () => {
|
|
const socket = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
|
);
|
|
socket.binaryType = "arraybuffer";
|
|
|
|
await waitFor(socket, "message"); // handshake
|
|
|
|
socket.send(Uint8Array.from([4, 1, 2, 3, 4]));
|
|
|
|
const { data } = await waitFor(socket, "message");
|
|
|
|
expect(data).to.eql(Uint8Array.from([4, 1, 2, 3, 4]).buffer);
|
|
|
|
socket.close();
|
|
});
|
|
|
|
it("closes the session upon invalid packet format", async () => {
|
|
const socket = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
|
);
|
|
|
|
await waitFor(socket, "message"); // handshake
|
|
|
|
socket.send("abc");
|
|
|
|
await waitFor(socket, "close");
|
|
|
|
socket.close();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("heartbeat", function () {
|
|
this.timeout(5000);
|
|
|
|
describe("HTTP long-polling", () => {
|
|
it("sends ping/pong packets", async () => {
|
|
const sid = await initLongPollingSession();
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
const pushResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
|
{
|
|
method: "post",
|
|
body: "1:2",
|
|
}
|
|
);
|
|
|
|
expect(pushResponse.status).to.eql(200);
|
|
|
|
const pollResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
|
);
|
|
|
|
expect(pollResponse.status).to.eql(200);
|
|
|
|
const pollContent = await pollResponse.text();
|
|
|
|
expect(pollContent).to.eql("1:3");
|
|
}
|
|
});
|
|
|
|
it("closes the session upon ping timeout", async () => {
|
|
const sid = await initLongPollingSession();
|
|
|
|
await sleep(PING_INTERVAL + PING_TIMEOUT);
|
|
|
|
const pushResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
|
{
|
|
method: "post",
|
|
body: "1:2",
|
|
}
|
|
);
|
|
|
|
expect(pushResponse.status).to.eql(400);
|
|
});
|
|
});
|
|
|
|
describe("WebSocket", () => {
|
|
it("sends ping/pong packets", async () => {
|
|
const socket = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
|
);
|
|
|
|
const x = await waitFor(socket, "message"); // handshake
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
socket.send("2");
|
|
|
|
const { data } = await waitFor(socket, "message");
|
|
|
|
expect(data).to.eql("3");
|
|
}
|
|
|
|
socket.close();
|
|
});
|
|
|
|
it("closes the session upon ping timeout", async () => {
|
|
const socket = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
|
);
|
|
|
|
await waitFor(socket, "close"); // handshake
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("close", () => {
|
|
describe("HTTP long-polling", () => {
|
|
it("forcefully closes the session", async () => {
|
|
const sid = await initLongPollingSession();
|
|
|
|
const [pollResponse] = await Promise.all([
|
|
fetch(`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`),
|
|
fetch(`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`, {
|
|
method: "post",
|
|
body: "1:1",
|
|
}),
|
|
]);
|
|
|
|
expect(pollResponse.status).to.eql(200);
|
|
|
|
const pullContent = await pollResponse.text();
|
|
|
|
expect(pullContent).to.eql("1:6");
|
|
|
|
const pollResponse2 = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
|
);
|
|
|
|
expect(pollResponse2.status).to.eql(400);
|
|
});
|
|
});
|
|
|
|
describe("WebSocket", () => {
|
|
it("forcefully closes the session", async () => {
|
|
const socket = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
|
);
|
|
|
|
await waitFor(socket, "message"); // handshake
|
|
|
|
socket.send("1");
|
|
|
|
await waitFor(socket, "close");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("upgrade", () => {
|
|
it("successfully upgrades from HTTP long-polling to WebSocket", async () => {
|
|
const sid = await initLongPollingSession();
|
|
|
|
const socket = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=3&transport=websocket&sid=${sid}`
|
|
);
|
|
|
|
await waitFor(socket, "open");
|
|
|
|
// send probe
|
|
socket.send("2probe");
|
|
|
|
const probeResponse = await waitFor(socket, "message");
|
|
|
|
expect(probeResponse.data).to.eql("3probe");
|
|
|
|
const pollResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
|
);
|
|
|
|
expect(pollResponse.status).to.eql(200);
|
|
|
|
const pollContent = await pollResponse.text();
|
|
|
|
expect(pollContent).to.eql("1:6"); // "noop" packet to cleanly end the HTTP long-polling request
|
|
|
|
// complete upgrade
|
|
socket.send("5");
|
|
|
|
socket.send("4hello");
|
|
|
|
const { data } = await waitFor(socket, "message");
|
|
|
|
expect(data).to.eql("4hello");
|
|
});
|
|
|
|
it("ignores HTTP requests with same sid after upgrade", async () => {
|
|
const sid = await initLongPollingSession();
|
|
|
|
const socket = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=3&transport=websocket&sid=${sid}`
|
|
);
|
|
|
|
await waitFor(socket, "open");
|
|
|
|
socket.send("2probe");
|
|
const res = await waitFor(socket, "message");
|
|
expect(res.data).to.eql("3probe");
|
|
|
|
socket.send("5");
|
|
|
|
const pollResponse = await fetch(
|
|
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
|
);
|
|
|
|
expect(pollResponse.status).to.eql(400);
|
|
|
|
socket.send("4hello");
|
|
|
|
const { data } = await waitFor(socket, "message");
|
|
|
|
expect(data).to.eql("4hello");
|
|
});
|
|
|
|
it("ignores WebSocket connection with same sid after upgrade", async () => {
|
|
const sid = await initLongPollingSession();
|
|
|
|
const socket = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=3&transport=websocket&sid=${sid}`
|
|
);
|
|
|
|
await waitFor(socket, "open");
|
|
|
|
socket.send("2probe");
|
|
const res = await waitFor(socket, "message");
|
|
expect(res.data).to.eql("3probe");
|
|
|
|
socket.send("5");
|
|
|
|
const socket2 = new WebSocket(
|
|
`${WS_URL}/engine.io/?EIO=3&transport=websocket&sid=${sid}`
|
|
);
|
|
|
|
await waitFor(socket2, "close");
|
|
|
|
socket.send("4hello");
|
|
|
|
const { data } = await waitFor(socket, "message");
|
|
|
|
expect(data).to.eql("4hello");
|
|
});
|
|
});
|
|
});
|