From d061da96d5c99ac602517438d2c4dfe8ce241509 Mon Sep 17 00:00:00 2001 From: Mihaly Lengyel Date: Mon, 11 Nov 2024 11:42:17 +0100 Subject: [PATCH] test: add the option for backend-sdk-testing to remotely control overrides --- test/test-server/package-lock.json | 72 +++++++++- test/test-server/package.json | 3 +- test/test-server/src/index.ts | 174 +++++++++++++++++++----- test/test-server/src/overrideLogging.ts | 2 +- 4 files changed, 215 insertions(+), 36 deletions(-) diff --git a/test/test-server/package-lock.json b/test/test-server/package-lock.json index 6e069597b..e733bc2e0 100644 --- a/test/test-server/package-lock.json +++ b/test/test-server/package-lock.json @@ -11,7 +11,8 @@ "debug": "^4.3.5", "express": "^4.19.2", "nock": "^13.5.4", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "ws": "^8.18.0" }, "devDependencies": { "tsx": "^4.11.2" @@ -438,6 +439,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -956,6 +971,18 @@ "node": ">= 10.13" } }, + "node_modules/node-gyp-build": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", + "optional": true, + "peer": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -1252,6 +1279,26 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } }, "dependencies": { @@ -1464,6 +1511,16 @@ } } }, + "bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "optional": true, + "peer": true, + "requires": { + "node-gyp-build": "^4.3.0" + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1843,6 +1900,13 @@ "propagate": "^2.0.0" } }, + "node-gyp-build": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", + "optional": true, + "peer": true + }, "object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -2051,6 +2115,12 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "requires": {} } } } diff --git a/test/test-server/package.json b/test/test-server/package.json index 4bf3e6d9d..11a8fc922 100644 --- a/test/test-server/package.json +++ b/test/test-server/package.json @@ -11,7 +11,8 @@ "debug": "^4.3.5", "express": "^4.19.2", "nock": "^13.5.4", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "ws": "^8.18.0" }, "devDependencies": { "tsx": "^4.11.2" diff --git a/test/test-server/src/index.ts b/test/test-server/src/index.ts index b2097452f..0dc10e1f1 100644 --- a/test/test-server/src/index.ts +++ b/test/test-server/src/index.ts @@ -51,10 +51,12 @@ import userMetadataRoutes from "./usermetadata"; import TOTPRoutes from "./totp"; import { getFunc, resetOverrideParams, getOverrideParams } from "./testFunctionMapper"; import OverrideableBuilder from "supertokens-js-override"; -import { resetOverrideLogs, logOverrideEvent, getOverrideLogs } from "./overrideLogging"; +import { resetOverrideLogs, logOverrideEvent, getOverrideLogs, transformLoggedData } from "./overrideLogging"; import Dashboard from "../../../recipe/dashboard"; import DashboardRecipe from "../../../lib/build/recipe/dashboard/recipe"; +import WebSocket from "ws"; + const { logDebugMessage } = logger("com.supertokens:node-test-server"); const API_PORT = Number(process.env.API_PORT || 3030); @@ -111,6 +113,10 @@ function initST(config: any) { ]; const parsedConfig = JSON.parse(config); + let ws: WebSocketOverrideController; + if (parsedConfig.wsPort) { + ws = new WebSocketOverrideController(new WebSocket(`ws://localhost:${parsedConfig.wsPort}`)); + } const init = { ...parsedConfig, }; @@ -130,14 +136,16 @@ function initST(config: any) { ...config?.emailDelivery, override: overrideBuilderWithLogging( "EmailPassword.emailDelivery.override", - config?.emailDelivery?.override + config?.emailDelivery?.override, + ws ), }, override: { - apis: overrideBuilderWithLogging("EmailPassword.override.apis", config?.override?.apis), + apis: overrideBuilderWithLogging("EmailPassword.override.apis", config?.override?.apis, ws), functions: overrideBuilderWithLogging( "EmailPassword.override.functions", - config?.override?.functions + config?.override?.functions, + ws ), }, }) @@ -148,10 +156,11 @@ function initST(config: any) { Session.init({ ...config, override: { - apis: overrideBuilderWithLogging("Session.override.apis", config?.override?.apis), + apis: overrideBuilderWithLogging("Session.override.apis", config?.override?.apis, ws), functions: overrideBuilderWithLogging( "Session.override.functions", - config?.override?.functions + config?.override?.functions, + ws ), }, }) @@ -166,13 +175,20 @@ function initST(config: any) { config.shouldDoAutomaticAccountLinking, { shouldAutomaticallyLink: false, - } + }, + ws + ), + onAccountLinked: callbackWithLog( + "AccountLinking.onAccountLinked", + config.onAccountLinked, + undefined, + ws ), - onAccountLinked: callbackWithLog("AccountLinking.onAccountLinked", config.onAccountLinked), override: { functions: overrideBuilderWithLogging( "AccountLinking.override.functions", - config?.override?.functions + config?.override?.functions, + ws ), }, }) @@ -196,10 +212,11 @@ function initST(config: any) { ThirdParty.init({ ...init, override: { - apis: overrideBuilderWithLogging("ThirdParty.override.apis", config?.override?.apis), + apis: overrideBuilderWithLogging("ThirdParty.override.apis", config?.override?.apis, ws), functions: overrideBuilderWithLogging( "ThirdParty.override.functions", - config?.override?.functions + config?.override?.functions, + ws ), }, }) @@ -213,19 +230,22 @@ function initST(config: any) { ...config?.emailDelivery, override: overrideBuilderWithLogging( "EmailVerification.emailDelivery", - config?.emailDelivery?.override + config?.emailDelivery?.override, + ws ), }, getEmailForRecipeUserId: callbackWithLog( "EmailVerification.getEmailForRecipeUserId", config?.getEmailForRecipeUserId, - { status: "UNKNOWN_USER_ID_ERROR" } + { status: "UNKNOWN_USER_ID_ERROR" }, + ws ), override: { - apis: overrideBuilderWithLogging("EmailVerification.override.apis", config?.override?.apis), + apis: overrideBuilderWithLogging("EmailVerification.override.apis", config?.override?.apis, ws), functions: overrideBuilderWithLogging( "EmailVerification.override.functions", - config?.override?.functions + config?.override?.functions, + ws ), }, }) @@ -236,10 +256,11 @@ function initST(config: any) { Multitenancy.init({ ...config, override: { - apis: overrideBuilderWithLogging("Multitenancy.override.apis", config?.override?.apis), + apis: overrideBuilderWithLogging("Multitenancy.override.apis", config?.override?.apis, ws), functions: overrideBuilderWithLogging( "Multitenancy.override.functions", - config?.override?.functions + config?.override?.functions, + ws ), }, }) @@ -251,7 +272,7 @@ function initST(config: any) { ...config, emailDelivery: { ...config?.emailDelivery, - override: overrideBuilderWithLogging("Passwordless.emailDelivery", undefined), + override: overrideBuilderWithLogging("Passwordless.emailDelivery", undefined, ws), service: { ...config?.emailDelivery?.service, sendEmail: @@ -261,7 +282,7 @@ function initST(config: any) { }, smsDelivery: { ...config?.smsDelivery, - override: overrideBuilderWithLogging("Passwordless.smsDelivery", undefined), + override: overrideBuilderWithLogging("Passwordless.smsDelivery", undefined, ws), service: { ...config?.smsDelivery?.service, sendSms: @@ -269,10 +290,11 @@ function initST(config: any) { }, }, override: { - apis: overrideBuilderWithLogging("Passwordless.override.apis", config?.override?.apis), + apis: overrideBuilderWithLogging("Passwordless.override.apis", config?.override?.apis, ws), functions: overrideBuilderWithLogging( "Passwordless.override.functions", - config?.override?.functions + config?.override?.functions, + ws ), }, }) @@ -283,10 +305,11 @@ function initST(config: any) { MultiFactorAuth.init({ ...config, override: { - apis: overrideBuilderWithLogging("MultiFactorAuth.override.apis", config?.override?.apis), + apis: overrideBuilderWithLogging("MultiFactorAuth.override.apis", config?.override?.apis, ws), functions: overrideBuilderWithLogging( "MultiFactorAuth.override.functions", - config?.override?.functions + config?.override?.functions, + ws ), }, }) @@ -297,8 +320,12 @@ function initST(config: any) { TOTP.init({ ...config, override: { - apis: overrideBuilderWithLogging("TOTP.override.apis", config?.override?.apis), - functions: overrideBuilderWithLogging("TOTP.override.functions", config?.override?.functions), + apis: overrideBuilderWithLogging("TOTP.override.apis", config?.override?.apis, ws), + functions: overrideBuilderWithLogging( + "TOTP.override.functions", + config?.override?.functions, + ws + ), }, }) ); @@ -308,10 +335,11 @@ function initST(config: any) { OAuth2Provider.init({ ...config, override: { - apis: overrideBuilderWithLogging("OAuth2Provider.override.apis", config?.override?.apis), + apis: overrideBuilderWithLogging("OAuth2Provider.override.apis", config?.override?.apis, ws), functions: overrideBuilderWithLogging( "OAuth2Provider.override.functions", - config?.override?.functions + config?.override?.functions, + ws ), }, }) @@ -360,7 +388,7 @@ app.get("/test/overrideparams", async (req, res, next) => { }); app.get("/test/featureflag", async (req, res, next) => { - res.json(["removedOverwriteSessionDuringSignInUp"]); + res.json(["removedOverwriteSessionDuringSignInUp", "remoteOverride"]); }); app.post("/test/resetoverrideparams", async (req, res, next) => { @@ -490,14 +518,25 @@ function loggingOverrideFunc(name: string, originalImpl: (...args: any[]) => }; } -function callbackWithLog(name: string, overrideName: string, defaultValue?: T) { - const impl = overrideName ? getFunc(overrideName) : () => defaultValue; +function callbackWithLog( + name: string, + overrideName: string, + defaultValue: T | undefined, + ws: WebSocketOverrideController | undefined +) { + const impl = + ws !== undefined + ? ws.getOverrideFunc(name, overrideName ? getFunc(overrideName) : () => defaultValue) + : overrideName + ? getFunc(overrideName) + : () => defaultValue; return loggingOverrideFunc(name, impl); } function overrideBuilderWithLogging any) | undefined>>( name: string, - overrideName?: string + overrideName: string | undefined, + ws: WebSocketOverrideController | undefined ) { return (originalImpl: T, builder: OverrideableBuilder) => { builder.override((oI) => { @@ -507,14 +546,83 @@ function overrideBuilderWithLogging for (const k of keys) { - ret[k] = loggingOverrideFunc(`${name}.${k}`, oI[k]!.bind(oI)); + const origImpl = oI[k]!.bind(oI); + ret[k] = loggingOverrideFunc( + `${name}.${k}`, + ws !== undefined && overrideName?.startsWith("ws:") && overrideName.includes(`|${k}|`) + ? ws.getOverrideFunc(k, origImpl) + : origImpl + ); } return ret; }); - if (overrideName !== undefined) { + if (overrideName !== undefined && !overrideName.startsWith("ws:")) { builder.override(getFunc(overrideName)); } return originalImpl; }; } + +class RemoteOverrideCall { + public promise: Promise; + public resolve: (value: unknown) => void; + public reject: (reason?: any) => void; + + constructor( + public ts: number, + public name: string, + public origArgs: any[], + public impl: (...args: any[]) => Promise + ) { + this.promise = new Promise((res, rej) => { + this.resolve = res; + this.reject = rej; + }); + } +} + +class WebSocketOverrideController { + private connectedPromise: Promise; + private waiting: RemoteOverrideCall[]; + + constructor(private ws: WebSocket) { + this.waiting = []; + this.connectedPromise = new Promise((resolve, reject) => { + ws.on("open", resolve); + ws.on("error", reject); + }); + + ws.on("message", async (msg) => { + const { name, type, ts, value } = JSON.parse(msg); + const info = this.waiting.find((def) => def.ts === ts && def.name === name); + if (!info) { + return; + } + if (type === "RET") { + // we'll need deserialization I think. + info.resolve(value); + } else if (type === "REJ") { + info.reject(value); + } else if (type === "OI") { + // we'll need deserialization I think. + const resp = await info.impl(...value); + ws.send(JSON.stringify({ name, type: "OI_RES", ts, value: transformLoggedData(resp) })); + } else if (type === "NO_INTERCEPT") { + const resp = await info.impl(...info.origArgs); + info.resolve(resp); + } + }); + } + + getOverrideFunc(name: string, originalImpl: (...args: any[]) => Promise) { + return async (...args: any[]) => { + const ts = Date.now(); + const def = new RemoteOverrideCall(ts, name, args, originalImpl); + this.waiting.push(def); + await this.connectedPromise; + this.ws.send(JSON.stringify({ name, type: "CALL", ts, args: transformLoggedData(args) })); + return def.promise; + }; + } +} diff --git a/test/test-server/src/overrideLogging.ts b/test/test-server/src/overrideLogging.ts index f8dd686e4..175c77bac 100644 --- a/test/test-server/src/overrideLogging.ts +++ b/test/test-server/src/overrideLogging.ts @@ -19,7 +19,7 @@ export function logOverrideEvent(name: string, type: "RES" | "REJ" | "CALL", dat }); } -function transformLoggedData(data: any, visited: Set = new Set()) { +export function transformLoggedData(data: any, visited: Set = new Set()) { if (typeof data !== "object") { return data; }