actionhero
Version:
The reusable, scalable, and quick node.js API server for stateless and stateful applications
676 lines (574 loc) • 21.7 kB
text/typescript
/**
* @jest-environment jsdom
*/
;
// we need to use 'use strict' here because we are relying on EVAL to load a variable
import { api, Process, config, chatRoom, utils } from "./../../src/index";
const actionhero = new Process();
let clientA;
let clientB;
let clientC;
let url;
const connectClients = async () => {
// get ActionheroWebsocketClient in scope
const ActionheroWebsocketClient = eval(
// @ts-ignore
api.servers.servers.websocket.compileActionheroWebsocketClientJS()
); // eslint-disable-line
const S = api.servers.servers.websocket.server.Socket;
url = "http://localhost:" + config.servers.web.port;
const clientAsocket = new S(url);
const clientBsocket = new S(url);
const clientCsocket = new S(url);
clientA = new ActionheroWebsocketClient({}, clientAsocket); // eslint-disable-line
clientB = new ActionheroWebsocketClient({}, clientBsocket); // eslint-disable-line
clientC = new ActionheroWebsocketClient({}, clientCsocket); // eslint-disable-line
await utils.sleep(100);
};
const awaitMethod = async (
client,
method,
returnsError = false
): Promise<{
[key: string]: any;
}> => {
return new Promise((resolve, reject) => {
client[method]((a, b) => {
if (returnsError && a) {
return reject(a);
}
if (returnsError) {
return resolve(b);
}
return resolve(a);
});
});
};
const awaitAction = async (client, action, params = {}): Promise<any> => {
return new Promise((resolve) => {
client.action(action, params, (response) => {
return resolve(response);
});
});
};
const awaitFile = async (client, file): Promise<any> => {
return new Promise((resolve) => {
client.file(file, (response) => {
return resolve(response);
});
});
};
const awaitRoom = async (client, method, room): Promise<any> => {
return new Promise((resolve) => {
client[method](room, (response) => {
return resolve(response);
});
});
};
describe("Server: Web Socket", () => {
beforeAll(async () => {
await actionhero.start();
await api.redis.clients.client.flushdb();
await api.redis.clients.client.flushdb();
await chatRoom.add("defaultRoom");
await chatRoom.add("otherRoom");
url = "http://localhost:" + config.servers.web.port;
config.servers.websocket.clientUrl = url;
await connectClients();
});
afterAll(async () => {
await actionhero.stop();
});
test("socket client connections should work: client 1", async () => {
const data = await awaitMethod(clientA, "connect", true);
expect(data.context).toEqual("response");
expect(data.data.totalActions).toEqual(0);
expect(clientA.welcomeMessage).toEqual(
"Hello! Welcome to the actionhero api"
);
});
test("socket client connections should work: client 2", async () => {
const data = await awaitMethod(clientB, "connect", true);
expect(data.context).toEqual("response");
expect(data.data.totalActions).toEqual(0);
expect(clientB.welcomeMessage).toEqual(
"Hello! Welcome to the actionhero api"
);
});
test("socket client connections should work: client 3", async () => {
const data = await awaitMethod(clientC, "connect", true);
expect(data.context).toEqual("response");
expect(data.data.totalActions).toEqual(0);
expect(clientC.welcomeMessage).toEqual(
"Hello! Welcome to the actionhero api"
);
});
describe("with connection", () => {
beforeAll(async () => {
await awaitMethod(clientA, "connect", true);
await awaitMethod(clientB, "connect", true);
await awaitMethod(clientC, "connect", true);
});
test("I can get my connection details", async () => {
const response = await awaitMethod(clientA, "detailsView");
expect(response.data.connectedAt).toBeLessThan(new Date().getTime());
expect(response.data.remoteIP).toEqual("127.0.0.1");
});
test("can run actions with errors", async () => {
const response = await awaitAction(clientA, "cacheTest");
expect(response.error).toEqual(
"key is a required parameter for this action"
);
});
test("properly handles duplicate room commands at the same time", async () => {
awaitRoom(clientA, "roomAdd", "defaultRoom");
awaitRoom(clientA, "roomAdd", "defaultRoom");
await utils.sleep(500);
expect(clientA.rooms).toEqual(["defaultRoom"]);
});
test("properly responds with messageId", async () => {
let aTime;
let bTime;
const startingMessageId = clientA.messageId;
awaitRoom(clientA, "roomAdd", "defaultRoom"); // fast
let responseA = awaitAction(clientA, "sleepTest"); // slow
awaitRoom(clientA, "roomAdd", "defaultRoom"); // fast
let responseB = awaitAction(clientA, "randomNumber"); // fast
responseA.then((data) => {
responseA = data;
aTime = new Date();
});
responseB.then((data) => {
responseB = data;
bTime = new Date();
});
await utils.sleep(2001);
//@ts-ignore
expect(responseA.messageId).toEqual(startingMessageId + 2);
//@ts-ignore
expect(responseB.messageId).toEqual(startingMessageId + 4);
expect(aTime.getTime()).toBeGreaterThan(bTime.getTime());
});
test("messageId can be configurable", async () => {
const response = await awaitAction(clientA, "randomNumber", {
messageId: "aaa",
});
expect(response.messageId).toBe("aaa");
});
test("can run actions properly without params", async () => {
const response = await awaitAction(clientA, "randomNumber");
expect(response.error).toBeUndefined();
expect(response.randomNumber).toBeTruthy();
});
test("can run actions properly with params", async () => {
const response = await awaitAction(clientA, "cacheTest", {
key: "test key",
value: "test value",
});
expect(response.error).toBeUndefined();
expect(response.cacheTestResults).toBeTruthy();
});
test("does not have sticky params", async () => {
const response = await awaitAction(clientA, "cacheTest", {
key: "test key",
value: "test value",
});
expect(response.cacheTestResults.loadResp.key).toEqual(
"cacheTest_test key"
);
expect(response.cacheTestResults.loadResp.value).toEqual("test value");
const responseAgain = await awaitAction(clientA, "cacheTest");
expect(responseAgain.error).toEqual(
"key is a required parameter for this action"
);
});
test("will limit how many simultaneous connections I can have", async () => {
const responses = [];
clientA.action("sleepTest", { sleepDuration: 100 }, (response) => {
responses.push(response);
});
clientA.action("sleepTest", { sleepDuration: 200 }, (response) => {
responses.push(response);
});
clientA.action("sleepTest", { sleepDuration: 300 }, (response) => {
responses.push(response);
});
clientA.action("sleepTest", { sleepDuration: 400 }, (response) => {
responses.push(response);
});
clientA.action("sleepTest", { sleepDuration: 500 }, (response) => {
responses.push(response);
});
clientA.action("sleepTest", { sleepDuration: 600 }, (response) => {
responses.push(response);
});
await utils.sleep(1000);
expect(responses).toHaveLength(6);
for (const i in responses) {
const response = responses[i];
if (i.toString() === "0") {
expect(response.error).toEqual("you have too many pending requests");
} else {
expect(response.error).toBeUndefined();
}
}
});
describe("files", () => {
test("can request file data", async () => {
const data = await awaitFile(clientA, "simple.html");
expect(data.error).toBeUndefined();
expect(data.content).toContain("<h1>Actionhero</h1>");
expect(data.mime).toEqual("text/html");
expect(data.length).toEqual(101);
});
test("missing files", async () => {
const data = await awaitFile(clientA, "missing.html");
expect(data.error).toEqual("That file is not found");
expect(data.mime).toEqual("text/html");
expect(data.content).toBeNull();
});
});
describe("chat", () => {
beforeAll(() => {
chatRoom.addMiddleware({
name: "join chat middleware",
join: async (connection, room) => {
await api.chatRoom.broadcast(
{},
room,
`I have entered the room: ${connection.id}`
);
},
});
chatRoom.addMiddleware({
name: "leave chat middleware",
leave: async (connection, room) => {
api.chatRoom.broadcast(
{},
room,
`I have left the room: ${connection.id}`
);
},
});
});
afterAll(() => {
api.chatRoom.middleware = {};
api.chatRoom.globalMiddleware = [];
});
beforeEach(async () => {
await awaitRoom(clientA, "roomAdd", "defaultRoom");
await awaitRoom(clientB, "roomAdd", "defaultRoom");
await awaitRoom(clientC, "roomAdd", "defaultRoom");
// timeout to skip welcome messages as clients join rooms
await utils.sleep(100);
});
afterEach(async () => {
await awaitRoom(clientA, "roomLeave", "defaultRoom");
await awaitRoom(clientB, "roomLeave", "defaultRoom");
await awaitRoom(clientC, "roomLeave", "defaultRoom");
await awaitRoom(clientA, "roomLeave", "otherRoom");
await awaitRoom(clientB, "roomLeave", "otherRoom");
await awaitRoom(clientC, "roomLeave", "otherRoom");
});
test("can change rooms and get room details", async () => {
await awaitRoom(clientA, "roomAdd", "otherRoom");
const response = await awaitMethod(clientA, "detailsView");
expect(response.error).toBeUndefined();
expect(response.data.rooms[0]).toEqual("defaultRoom");
expect(response.data.rooms[1]).toEqual("otherRoom");
const roomResponse = await awaitRoom(clientA, "roomView", "otherRoom");
expect(roomResponse.data.membersCount).toEqual(1);
});
test("will update client room info when they change rooms", async () => {
expect(clientA.rooms[0]).toEqual("defaultRoom");
expect(clientA.rooms[1]).toBeUndefined();
const response = await awaitRoom(clientA, "roomAdd", "otherRoom");
expect(response.error).toBeUndefined();
expect(clientA.rooms[0]).toEqual("defaultRoom");
expect(clientA.rooms[1]).toEqual("otherRoom");
const leaveResponse = await awaitRoom(
clientA,
"roomLeave",
"defaultRoom"
);
expect(leaveResponse.error).toBeUndefined();
expect(clientA.rooms[0]).toEqual("otherRoom");
expect(clientA.rooms[1]).toBeUndefined();
});
test("clients can talk to each other", async () => {
await new Promise((resolve) => {
const listener = (response) => {
clientA.removeListener("say", listener);
expect(response.context).toEqual("user");
expect(response.message).toEqual("hello from client 2");
resolve(null);
};
clientA.on("say", listener);
clientB.say("defaultRoom", "hello from client 2");
});
});
test("The client say method does not rely on argument order", async () => {
await new Promise((resolve) => {
const listener = (response) => {
clientA.removeListener("say", listener);
expect(response.context).toEqual("user");
expect(response.message).toEqual("hello from client 2");
resolve(null);
};
clientB.say = (room, message) => {
clientB.send({ message: message, room: room, event: "say" });
};
clientA.on("say", listener);
clientB.say("defaultRoom", "hello from client 2");
});
});
test("connections are notified when I join a room", async () => {
await new Promise((resolve) => {
const listener = (response) => {
clientA.removeListener("say", listener);
expect(response.context).toEqual("user");
expect(response.message).toEqual(
"I have entered the room: " + clientB.id
);
resolve(null);
};
clientA.roomAdd("otherRoom", () => {
clientA.on("say", listener);
clientB.roomAdd("otherRoom");
});
});
});
test("connections are notified when I leave a room", async () => {
await new Promise((resolve) => {
const listener = (response) => {
clientA.removeListener("say", listener);
expect(response.context).toEqual("user");
expect(response.message).toEqual(
"I have left the room: " + clientB.id
);
resolve(null);
};
clientA.on("say", listener);
clientB.roomLeave("defaultRoom");
});
});
test("will not get messages for rooms I am not in", async () => {
const response = await awaitRoom(clientB, "roomAdd", "otherRoom");
expect(response.error).toBeUndefined();
expect(clientB.rooms.length).toEqual(2);
expect(clientC.rooms.length).toEqual(1);
const listener = (response) => {
clientC.removeListener("say", listener);
throw new Error("should not get here");
};
clientC.on("say", listener);
clientB.say("otherRoom", "you should not hear this");
await utils.sleep(1000);
clientC.removeListener("say", listener);
});
test("connections can see member counts changing within rooms as folks join and leave", async () => {
const response = await awaitRoom(clientA, "roomView", "defaultRoom");
expect(response.data.membersCount).toEqual(3);
await awaitRoom(clientB, "roomLeave", "defaultRoom");
const responseAgain = await awaitRoom(
clientA,
"roomView",
"defaultRoom"
);
expect(responseAgain.data.membersCount).toEqual(2);
});
describe("middleware - say and onSayReceive", () => {
afterEach(() => {
api.chatRoom.middleware = {};
api.chatRoom.globalMiddleware = [];
});
test("each listener receive custom message", async () => {
let messagesReceived = 0;
chatRoom.addMiddleware({
name: "say for each",
say: async (connection, room, messagePayload) => {
messagePayload.message += " - To: " + connection.id;
return messagePayload;
},
});
const listenerA = (response) => {
messagesReceived++;
clientA.removeListener("say", listenerA);
expect(response.message).toEqual(
"Test Message - To: " + clientA.id
); // clientA.id (Receiver)
};
const listenerB = (response) => {
messagesReceived++;
clientB.removeListener("say", listenerB);
expect(response.message).toEqual(
"Test Message - To: " + clientB.id
); // clientB.id (Receiver)
};
const listenerC = (response) => {
messagesReceived++;
clientC.removeListener("say", listenerC);
expect(response.message).toEqual(
"Test Message - To: " + clientC.id
); // clientC.id (Receiver)
};
clientA.on("say", listenerA);
clientB.on("say", listenerB);
clientC.on("say", listenerC);
clientB.say("defaultRoom", "Test Message");
await utils.sleep(1000);
expect(messagesReceived).toEqual(3);
});
test("only one message should be received per connection", async () => {
let firstSayCall = true;
chatRoom.addMiddleware({
name: "first say middleware",
say: async (connection, room, messagePayload) => {
if (firstSayCall) {
firstSayCall = false;
await utils.sleep(200);
}
},
});
let messagesReceived = 0;
const listenerA = () => {
clientA.removeListener("say", listenerA);
messagesReceived += 1;
};
const listenerB = () => {
clientB.removeListener("say", listenerB);
messagesReceived += 2;
};
const listenerC = () => {
clientC.removeListener("say", listenerC);
messagesReceived += 4;
};
clientA.on("say", listenerA);
clientB.on("say", listenerB);
clientC.on("say", listenerC);
clientB.say("defaultRoom", "Test Message");
await utils.sleep(1000);
expect(messagesReceived).toEqual(7);
});
test("each listener receive same custom message", async () => {
let messagesReceived = 0;
chatRoom.addMiddleware({
name: "say for each",
onSayReceive: (connection, room, messagePayload) => {
messagePayload.message += " - To: " + connection.id;
return messagePayload;
},
});
const listenerA = (response) => {
messagesReceived++;
clientA.removeListener("say", listenerA);
expect(response.message).toEqual(
"Test Message - To: " + clientB.id
); // clientB.id (Sender)
};
const listenerB = (response) => {
messagesReceived++;
clientB.removeListener("say", listenerB);
expect(response.message).toEqual(
"Test Message - To: " + clientB.id
); // clientB.id (Sender)
};
const listenerC = (response) => {
messagesReceived++;
clientC.removeListener("say", listenerC);
expect(response.message).toEqual(
"Test Message - To: " + clientB.id
); // clientB.id (Sender)
};
clientA.on("say", listenerA);
clientB.on("say", listenerB);
clientC.on("say", listenerC);
clientB.say("defaultRoom", "Test Message");
await utils.sleep(1000);
expect(messagesReceived).toEqual(3);
});
test("blocking middleware return an error", async () => {
chatRoom.addMiddleware({
name: "blocking chat middleware",
join: (connection, room) => {
throw new Error("joining rooms blocked");
},
});
const joinResponse = await awaitRoom(clientA, "roomAdd", "otherRoom");
expect(joinResponse.error).toEqual("Error: joining rooms blocked");
expect(joinResponse.status).toEqual("Error: joining rooms blocked");
});
});
});
describe("param collisions", () => {
let originalSimultaneousActions;
beforeAll(() => {
originalSimultaneousActions = config.general.simultaneousActions;
config.general.simultaneousActions = 99999999;
});
afterAll(() => {
config.general.simultaneousActions = originalSimultaneousActions;
});
test("will not have param collisions", async () => {
let completed = 0;
let started = 0;
const sleeps = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110];
await new Promise((resolve) => {
const toComplete = (sleep, response) => {
expect(sleep).toEqual(response.sleepDuration);
completed++;
if (completed === started) {
resolve(null);
}
};
sleeps.forEach((sleep) => {
started++;
clientA.action(
"sleepTest",
{ sleepDuration: sleep },
(response) => {
toComplete(sleep, response);
}
);
});
});
});
});
describe("disconnect", () => {
beforeEach(async () => {
try {
clientA.disconnect();
clientB.disconnect();
clientC.disconnect();
} catch (e) {}
await connectClients();
clientA.connect();
clientB.connect();
clientC.connect();
await utils.sleep(500);
});
test("client can disconnect", async () => {
expect(api.servers.servers.websocket.connections().length).toEqual(3);
clientA.disconnect();
clientB.disconnect();
clientC.disconnect();
await utils.sleep(500);
expect(api.servers.servers.websocket.connections().length).toEqual(0);
});
test("can be sent disconnect events from the server", async () => {
const response = await awaitMethod(clientA, "detailsView");
expect(response.data.remoteIP).toEqual("127.0.0.1");
let count = 0;
for (const id in api.connections.connections) {
count++;
api.connections.connections[id].destroy();
}
expect(count).toEqual(3);
clientA.detailsView(() => {
throw new Error("should not get response");
});
await utils.sleep(500);
});
});
});
});