UNPKG

actionhero

Version:

The reusable, scalable, and quick node.js API server for stateless and stateful applications

676 lines (574 loc) 21.7 kB
/** * @jest-environment jsdom */ "use strict"; // 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); }); }); }); });