From 4ec75cc7e3eb4c343bc3d98314cfe134af7653f2 Mon Sep 17 00:00:00 2001 From: Ahmad Abdulkareem Date: Tue, 16 Jun 2026 14:04:20 +0100 Subject: [PATCH 1/2] Implement StreamsGateway WebSocket integration test --- api/package.json | 1 + api/src/gateways/streams.gateway.spec.ts | 267 +++++++++++++++++++++++ api/src/gateways/streams.gateway.ts | 46 +++- package-lock.json | 120 ++++++---- 4 files changed, 380 insertions(+), 54 deletions(-) create mode 100644 api/src/gateways/streams.gateway.spec.ts diff --git a/api/package.json b/api/package.json index e3f785b..df60186 100644 --- a/api/package.json +++ b/api/package.json @@ -38,6 +38,7 @@ }, "devDependencies": { "@nestjs/cli": "^10.3.0", + "@nestjs/testing": "^10.4.22", "@types/bcrypt": "^6.0.0", "@types/compression": "^1.8.1", "@types/express": "^5.0.6", diff --git a/api/src/gateways/streams.gateway.spec.ts b/api/src/gateways/streams.gateway.spec.ts new file mode 100644 index 0000000..bf90984 --- /dev/null +++ b/api/src/gateways/streams.gateway.spec.ts @@ -0,0 +1,267 @@ +import { Test } from "@nestjs/testing" +import { JwtService } from "@nestjs/jwt" +import { StreamsGateway } from "./streams.gateway" +import { STREAM_EVENTS } from "./stream-events" + +type FakeHandshake = { + auth?: Record + headers?: Record + query?: Record +} + +type FakeSocket = { + id: string + handshake: FakeHandshake + data: Record + join: jest.Mock + leave: jest.Mock + emit: jest.Mock + disconnect: jest.Mock +} + +type FakeServer = { + to: jest.Mock +} + +function makeSocket(overrides: Partial = {}): FakeSocket { + return { + id: "socket-1", + handshake: { auth: {}, headers: {}, query: {} }, + data: {}, + join: jest.fn(async () => {}), + leave: jest.fn(async () => {}), + emit: jest.fn(() => {}), + disconnect: jest.fn(() => {}), + ...overrides, + } +} + +function makeServer(): { + server: FakeServer + events: Array<{ room: string; event: string; payload: unknown }> +} { + const events: Array<{ room: string; event: string; payload: unknown }> = [] + const server: FakeServer = { + to: jest.fn().mockImplementation((room: string) => ({ + emit: jest.fn((event: string, payload: unknown) => { + events.push({ room, event, payload }) + }), + })), + } + return { server, events } +} + +describe("StreamsGateway", () => { + let gateway: StreamsGateway + let jwtService: { verifyAsync: jest.Mock } + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + StreamsGateway, + { provide: JwtService, useValue: { verifyAsync: jest.fn() } }, + ], + }).compile() + + gateway = module.get(StreamsGateway) + jwtService = module.get(JwtService) as unknown as { verifyAsync: jest.Mock } + }) + + describe("handleConnection", () => { + it("connects a client with a valid JWT", async () => { + const socket = makeSocket({ + handshake: { auth: { token: "valid-token" } }, + }) + jwtService.verifyAsync.mockResolvedValue({ sub: 42 }) + + await gateway.handleConnection(socket as unknown as any) + + expect(jwtService.verifyAsync).toHaveBeenCalledWith("valid-token") + expect(socket.data.userId).toBe(42) + expect(socket.emit).toHaveBeenCalledWith("connected", { userId: 42 }) + expect(socket.disconnect).not.toHaveBeenCalled() + }) + + it("rejects a client with an invalid JWT", async () => { + const socket = makeSocket({ handshake: { auth: { token: "bad-token" } } }) + jwtService.verifyAsync.mockRejectedValue(new Error("jwt malformed")) + + await gateway.handleConnection(socket as unknown as any) + + expect(socket.emit).toHaveBeenCalledWith( + STREAM_EVENTS.ERROR, + expect.objectContaining({ + code: "INVALID_TOKEN", + message: expect.stringContaining("JWT verification failed"), + }), + ) + expect(socket.disconnect).toHaveBeenCalledWith(true) + }) + + it("rejects a client with no token", async () => { + const socket = makeSocket({ handshake: { auth: {} } }) + + await gateway.handleConnection(socket as unknown as any) + + expect(socket.emit).toHaveBeenCalledWith( + STREAM_EVENTS.ERROR, + expect.objectContaining({ + code: "MISSING_TOKEN", + message: expect.stringContaining("Authentication token required"), + }), + ) + expect(socket.disconnect).toHaveBeenCalledWith(true) + expect(jwtService.verifyAsync).not.toHaveBeenCalled() + }) + + it("accepts a token from the Authorization header fallback", async () => { + const socket = makeSocket({ + handshake: { + auth: {}, + headers: { authorization: "Bearer header-token" }, + }, + }) + jwtService.verifyAsync.mockResolvedValue({ sub: "user-99" }) + + await gateway.handleConnection(socket as unknown as any) + + expect(jwtService.verifyAsync).toHaveBeenCalledWith("header-token") + expect(socket.emit).toHaveBeenCalledWith("connected", { + userId: "user-99", + }) + }) + }) + + describe("stream room lifecycle", () => { + it("allows an authenticated client to subscribe", () => { + const socket = makeSocket({ data: { userId: 55 } }) + const result = gateway.handleSubscribe(socket as unknown as any, { + streamId: "abc", + }) + + expect(result).toEqual({ ok: true, room: "stream:abc" }) + expect(socket.join).toHaveBeenCalledWith("stream:abc") + }) + + it("rejects an unauthenticated client from subscribing", () => { + const socket = makeSocket({ data: {} }) + const result = gateway.handleSubscribe(socket as unknown as any, { + streamId: "abc", + }) + + expect(result).toEqual({ ok: false, error: "unauthenticated" }) + expect(socket.join).not.toHaveBeenCalled() + }) + + it("rejects an authenticated client from subscribing without a streamId", () => { + const socket = makeSocket({ data: { userId: 55 } }) + const result = gateway.handleSubscribe(socket as unknown as any, {}) + + expect(result).toEqual({ ok: false, error: "streamId required" }) + expect(socket.join).not.toHaveBeenCalled() + }) + + it("allows an authenticated client to unsubscribe", () => { + const socket = makeSocket({ data: { userId: 55 } }) + const result = gateway.handleUnsubscribe(socket as unknown as any, { + streamId: "abc", + }) + + expect(result).toEqual({ ok: true, room: "stream:abc" }) + expect(socket.leave).toHaveBeenCalledWith("stream:abc") + }) + + it("rejects an unauthenticated client from unsubscribing", () => { + const socket = makeSocket({ data: {} }) + const result = gateway.handleUnsubscribe(socket as unknown as any, { + streamId: "abc", + }) + + expect(result).toEqual({ ok: false, error: "unauthenticated" }) + expect(socket.leave).not.toHaveBeenCalled() + }) + + it("rejects unsubscribe calls without a streamId", () => { + const socket = makeSocket({ data: { userId: 55 } }) + const result = gateway.handleUnsubscribe(socket as unknown as any, {}) + + expect(result).toEqual({ ok: false, error: "streamId required" }) + expect(socket.leave).not.toHaveBeenCalled() + }) + + it("supports duplicate subscriptions without failure", () => { + const socket = makeSocket({ data: { userId: 55 } }) + gateway.handleSubscribe(socket as unknown as any, { streamId: "abc" }) + const second = gateway.handleSubscribe(socket as unknown as any, { + streamId: "abc", + }) + + expect(second).toEqual({ ok: true, room: "stream:abc" }) + expect(socket.join).toHaveBeenCalledTimes(2) + }) + }) + + describe("emit helpers", () => { + it("broadcasts only to the correct stream room", () => { + const { server, events } = makeServer() + gateway.server = server as unknown as any + + gateway.emitStarted({ + streamId: "1", + userId: 1, + startedAt: "2026-06-16T00:00:00Z", + }) + gateway.emitStopped({ + streamId: "2", + userId: 2, + stoppedAt: "2026-06-16T00:00:00Z", + }) + gateway.emitError({ + streamId: "3", + occurredAt: "2026-06-16T00:00:00Z", + code: "ERR", + message: "boom", + }) + + expect(events).toEqual([ + { + room: "stream:1", + event: STREAM_EVENTS.STARTED, + payload: { + streamId: "1", + userId: 1, + startedAt: "2026-06-16T00:00:00Z", + }, + }, + { + room: "stream:2", + event: STREAM_EVENTS.STOPPED, + payload: { + streamId: "2", + userId: 2, + stoppedAt: "2026-06-16T00:00:00Z", + }, + }, + { + room: "stream:3", + event: STREAM_EVENTS.ERROR, + payload: { + streamId: "3", + occurredAt: "2026-06-16T00:00:00Z", + code: "ERR", + message: "boom", + }, + }, + ]) + }) + }) + + describe("handleDisconnect", () => { + it("does not throw when a socket disconnects", () => { + const socket = makeSocket({ data: { userId: 99 } }) + expect(() => + gateway.handleDisconnect(socket as unknown as any), + ).not.toThrow() + }) + }) +}) diff --git a/api/src/gateways/streams.gateway.ts b/api/src/gateways/streams.gateway.ts index c09d8d9..b6c2147 100644 --- a/api/src/gateways/streams.gateway.ts +++ b/api/src/gateways/streams.gateway.ts @@ -69,7 +69,11 @@ export class StreamsGateway try { const token = this.extractToken(client) if (!token) { - this.disconnectWithError(client, "MISSING_TOKEN", "Authentication token required") + this.disconnectWithError( + client, + "MISSING_TOKEN", + "Authentication token required", + ) return } @@ -81,8 +85,13 @@ export class StreamsGateway ) client.emit("connected", { userId: payload.sub }) } catch (err) { - const message = err instanceof Error ? err.message : "unknown verification error" - this.disconnectWithError(client, "INVALID_TOKEN", `JWT verification failed: ${message}`) + const message = + err instanceof Error ? err.message : "unknown verification error" + this.disconnectWithError( + client, + "INVALID_TOKEN", + `JWT verification failed: ${message}`, + ) } } @@ -126,6 +135,9 @@ export class StreamsGateway @ConnectedSocket() client: AuthenticatedSocket, payload: { streamId?: string | number } = {}, ): { ok: boolean; room?: string; error?: string } { + if (!client.data?.userId) { + return { ok: false, error: "unauthenticated" } + } if (payload.streamId === undefined || payload.streamId === null) { return { ok: false, error: "streamId required" } } @@ -141,15 +153,21 @@ export class StreamsGateway * ------------------------------------------------------------------ */ emitStarted(payload: StreamStartedPayload): void { - this.server.to(this.roomFor(payload.streamId)).emit(STREAM_EVENTS.STARTED, payload) + this.server + .to(this.roomFor(payload.streamId)) + .emit(STREAM_EVENTS.STARTED, payload) } emitStopped(payload: StreamStoppedPayload): void { - this.server.to(this.roomFor(payload.streamId)).emit(STREAM_EVENTS.STOPPED, payload) + this.server + .to(this.roomFor(payload.streamId)) + .emit(STREAM_EVENTS.STOPPED, payload) } emitError(payload: StreamErrorPayload): void { - this.server.to(this.roomFor(payload.streamId)).emit(STREAM_EVENTS.ERROR, payload) + this.server + .to(this.roomFor(payload.streamId)) + .emit(STREAM_EVENTS.ERROR, payload) } /* -------------------------------------------------------------- */ @@ -160,7 +178,10 @@ export class StreamsGateway private extractToken(client: Socket): string | null { // Preferred: socket.io handshake auth payload — `io(url, { auth: { token }})` - const handshakeAuth = (client.handshake?.auth ?? {}) as Record + const handshakeAuth = (client.handshake?.auth ?? {}) as Record< + string, + unknown + > const rawAuthToken = handshakeAuth["token"] if (typeof rawAuthToken === "string" && rawAuthToken.length > 0) { return rawAuthToken @@ -168,7 +189,10 @@ export class StreamsGateway // Fallback: `Authorization: Bearer ` header. const authHeader = client.handshake?.headers?.authorization - if (typeof authHeader === "string" && authHeader.toLowerCase().startsWith("bearer ")) { + if ( + typeof authHeader === "string" && + authHeader.toLowerCase().startsWith("bearer ") + ) { return authHeader.slice(7).trim() || null } @@ -182,7 +206,11 @@ export class StreamsGateway return null } - private disconnectWithError(client: Socket, code: string, message: string): void { + private disconnectWithError( + client: Socket, + code: string, + message: string, + ): void { this.logger.warn(`rejecting client ${client.id}: [${code}] ${message}`) client.emit(STREAM_EVENTS.ERROR, { streamId: "", diff --git a/package-lock.json b/package-lock.json index f014b2a..80e8bdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ }, "devDependencies": { "@nestjs/cli": "^10.3.0", + "@nestjs/testing": "^10.4.22", "@types/bcrypt": "^6.0.0", "@types/compression": "^1.8.1", "@types/express": "^5.0.6", @@ -423,7 +424,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1072,7 +1072,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1096,7 +1095,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2722,7 +2720,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "20.4.1", "iterare": "1.2.1", @@ -2754,7 +2751,6 @@ "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -3270,6 +3266,34 @@ } } }, + "node_modules/@nestjs/testing": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.22.tgz", + "integrity": "sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, "node_modules/@nestjs/throttler": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz", @@ -3286,7 +3310,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.22.tgz", "integrity": "sha512-OLd4i0Faq7vgdtB5vVUrJ54hWEtcXy9poJ6n7kbbh/5ms+KffUl+wwGsbe7uSXLrkoyI8xXU6fZPkFArI+XiRg==", "license": "MIT", - "peer": true, "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -5204,6 +5227,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -5217,6 +5241,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -5231,7 +5256,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", @@ -5382,7 +5408,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5675,7 +5702,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5712,7 +5738,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5723,7 +5748,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5876,7 +5900,6 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -6342,6 +6365,7 @@ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", + "peer": true, "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -6355,6 +6379,7 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -6365,7 +6390,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6412,7 +6436,6 @@ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -6978,7 +7001,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -7076,7 +7098,6 @@ "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.7.6.tgz", "integrity": "sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==", "license": "MIT", - "peer": true, "dependencies": { "eventemitter3": "^5.0.1", "lodash.clonedeep": "^4.5.0", @@ -7234,7 +7255,6 @@ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -7289,8 +7309,7 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-variance-authority": { "version": "0.7.1", @@ -7605,6 +7624,7 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -7642,6 +7662,7 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.6.0" } @@ -8160,7 +8181,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -8285,8 +8307,7 @@ "version": "8.5.1", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz", "integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.5.1", @@ -8553,7 +8574,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8964,6 +8984,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -9007,6 +9028,7 @@ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", + "peer": true, "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", @@ -9031,6 +9053,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -9048,6 +9071,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -9064,6 +9088,7 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -9072,13 +9097,15 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/express/node_modules/raw-body": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", + "peer": true, "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", @@ -9094,6 +9121,7 @@ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", + "peer": true, "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", @@ -9112,6 +9140,7 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -9278,6 +9307,7 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -9299,6 +9329,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -9315,7 +9346,8 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/find-up": { "version": "4.1.0", @@ -9482,6 +9514,7 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -10325,7 +10358,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/is-stream": { "version": "2.0.1", @@ -10499,7 +10533,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -11397,7 +11430,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -12066,6 +12098,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -12150,6 +12183,7 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -12234,6 +12268,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", + "peer": true, "dependencies": { "mime-db": "^1.54.0" }, @@ -12385,7 +12420,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz", "integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.0.10", "@swc/helpers": "0.5.15", @@ -12920,7 +12954,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -13074,7 +13107,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13324,7 +13356,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13365,7 +13396,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13378,7 +13408,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.76.0.tgz", "integrity": "sha512-eKtLGgFeSgkHqQD8J59AMZ9a4uD1D83iSIzt4YlTGD7liDen5rrjcUO1rVIGd9yC1gofryjtHbv+4ny4hkLWlw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -13618,8 +13647,7 @@ "version": "0.1.14", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/repeat-string": { "version": "1.6.1", @@ -13788,6 +13816,7 @@ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -13804,6 +13833,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -13820,13 +13850,15 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/router/node_modules/path-to-regexp": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" @@ -13878,7 +13910,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -13980,7 +14011,6 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14026,6 +14056,7 @@ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", @@ -14052,6 +14083,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -14068,13 +14100,15 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", + "peer": true, "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -14822,8 +14856,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -15201,7 +15234,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15368,7 +15400,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15678,7 +15709,6 @@ "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", From 55e9799ab812ea728e29ef32ced646eec103582d Mon Sep 17 00:00:00 2001 From: ahmadogo Date: Tue, 16 Jun 2026 14:28:06 +0100 Subject: [PATCH 2/2] Implement StreamsGateway WebSocket integration test --- api/src/audit/admin-audit.controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/audit/admin-audit.controller.ts b/api/src/audit/admin-audit.controller.ts index 719ea2b..457ac33 100644 --- a/api/src/audit/admin-audit.controller.ts +++ b/api/src/audit/admin-audit.controller.ts @@ -6,6 +6,7 @@ import { PaginationQueryDto } from "../common/dto/pagination.dto" export class AdminAuditController { constructor(private readonly auditService: AuditService) {} + //get function @Get() async findAll(@Query() query: PaginationQueryDto) { const page = query.page ?? 1