From 02448eb6bebfc37677802daa7883326cfbaa0e83 Mon Sep 17 00:00:00 2001 From: Namit Nathwani Date: Tue, 10 Jun 2025 22:42:50 +0530 Subject: [PATCH 1/6] feat: adds test to ensure cookies are set correctly --- api-mock/fetcher.ts | 6 +- api-mock/mocks/SessionMock.ts | 24 ++++- test/session/session.test.js | 163 ++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 test/session/session.test.js diff --git a/api-mock/fetcher.ts b/api-mock/fetcher.ts index 3aa5174..c14f1a2 100644 --- a/api-mock/fetcher.ts +++ b/api-mock/fetcher.ts @@ -7,7 +7,7 @@ export const API_PORT = Number(process.env.API_PORT || 3030); let apiStatus: "NOT_READY" | "OK" = "NOT_READY"; let stConfig: string; -const fdiVersion = "4.0"; +export const fdiVersion = "4.1"; type Callback = ( error: any | null, @@ -186,6 +186,10 @@ export function setMockStatus(newStatus: "NOT_READY" | "OK") { apiStatus = newStatus; } +export function getMockStatus() { + return apiStatus; +} + export function setSTConfig(config) { stConfig = config; } diff --git a/api-mock/mocks/SessionMock.ts b/api-mock/mocks/SessionMock.ts index 1708d96..f98023c 100644 --- a/api-mock/mocks/SessionMock.ts +++ b/api-mock/mocks/SessionMock.ts @@ -222,7 +222,7 @@ class RemoteSessionObject implements SessionContainerInterface { } } -function deserializeSession(session) { +export function deserializeSession(session) { if (!session) return session; return new RemoteSessionObject(session); } @@ -273,6 +273,28 @@ export const SessionMock: Partial = { }); return deserializeSession(response); }, + createNewSession: async ( + req, + res, + tenantId, + recipeUserId, + accessTokenPayload, + sessionDataInDatabase, + userContext + ) => { + const response = await queryAPI({ + method: "post", + path: "/test/session/createnewsession", + input: { + tenantId, + recipeUserId: recipeUserId.getAsString(), + accessTokenPayload, + sessionDataInDatabase, + userContext, + }, + }); + return deserializeSession(response); + }, getSessionWithoutRequestResponse: async ( accessToken: string, antiCsrfToken?: string, diff --git a/test/session/session.test.js b/test/session/session.test.js new file mode 100644 index 0000000..d75fd45 --- /dev/null +++ b/test/session/session.test.js @@ -0,0 +1,163 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { printPath, createCoreApplication } = require("../utils"); +const assert = require("assert"); +const { recipesMock } = require("../../api-mock"); +const { EmailPassword, Session, supertokens } = recipesMock; +const SuperTokens = require("supertokens-node"); +const { getMockStatus, initApp, fdiVersion, API_PORT } = require("../../build/api-mock/fetcher"); +const { deserializeSession } = require("../../build/api-mock/mocks/SessionMock"); +const setCookieParser = require("set-cookie-parser"); +const { User: UserClass } = require("supertokens-node/lib/build/user"); +const fetch = require("cross-fetch"); + +// Re-defined functions since these tests require response headers to be parsed +async function queryAPI({ method, path, input, headers, returnResponse, skipInit }) { + if (!skipInit && getMockStatus() === "NOT_READY") { + await initApp(); + } + try { + let response = await fetch(`http://localhost:${API_PORT}${path}`, { + method, + headers: { + "Content-Type": "application/json", + "fdi-version": fdiVersion, + ...headers, + }, + body: JSON.stringify(input), + }); + + if (returnResponse) { + return { response, headers: response.headers }; + } + + if (!response.ok) { + throw response; + } + + return { + response: await response.json().catch(() => undefined), + headers: response.headers, + }; + } catch (error) { + console.log(error); + throw await error.json().catch(() => undefined); + } +} + +const signUp = async (tenantId, email, password, session, userContext) => { + const { response } = await queryAPI({ + method: "post", + path: "/test/emailpassword/signup", + input: { tenantId, email, password, session, userContext }, + }); + return { + ...response, + ...("user" in response + ? { + user: new UserClass(response.user), + } + : {}), + ...("recipeUserId" in response + ? { + recipeUserId: SuperTokens.convertToRecipeUserId(response.recipeUserId), + } + : {}), + }; +}; + +const createNewSession = async ( + tenantId, + recipeUserId, + accessTokenPayload, + sessionDataInDatabase, + disableAntiCsrf, + userContext +) => { + const { response, headers } = await queryAPI({ + method: "post", + path: "/test/session/createnewsession", + input: { + tenantId, + recipeUserId: recipeUserId.getAsString(), + accessTokenPayload, + sessionDataInDatabase, + disableAntiCsrf, + userContext, + }, + }); + return { session: deserializeSession(response), headers }; +}; + +describe(`sessionTests: ${printPath("[test/session/session.test.js]")}`, function () { + describe("Cookie checks", function () { + it("create new session", async function () { + const connectionURI = await createCoreApplication(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: "cookie" })], + }); + + const signUpResponse = await signUp("public", "test@example.com", "password123"); + const epUser = signUpResponse.user; + + // Create a new session for the user, get headers from the response + const { session, headers } = await createNewSession("public", epUser.loginMethods[0].recipeUserId); + + let cookies = headers.get("set-cookie"); + if (!Array.isArray(cookies)) { + cookies = [cookies]; + } + + // Parse cookies from the response. Fastapi set-cookie responses are a large string. + cookies = cookies + .flat() // Ensure we have a flat array of cookies + // Split cookie strings into arrays + .map((cookieStr) => setCookieParser.splitCookiesString(cookieStr)) + .flat() // Since we have an array of arrays now + // `parse` the cookies + .map(setCookieParser.parseString); + + const accessTokenCookie = cookies.find((info) => (info?.key ?? info?.name) == "sAccessToken"); + const refreshTokenCookie = cookies.find((info) => (info?.key ?? info?.name) === "sRefreshToken"); + + // Ensure cookies are set and with GMT timezones + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Date - Date headers are always GMT + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#expiresdate - Invalid cookies become session cookies + assert(accessTokenCookie, "Access token cookie not found"); + assert(accessTokenCookie.expires, "Access token cookie expiry not set"); + assert( + new Date(accessTokenCookie.expires).toUTCString().endsWith("GMT"), + "Access token cookie expiry is not in GMT" + ); + + assert(refreshTokenCookie, "Refresh token cookie not found"); + assert(refreshTokenCookie.expires, "Refresh token cookie expiry not set"); + assert( + new Date(refreshTokenCookie.expires).toUTCString().endsWith("GMT"), + "Refresh token cookie expiry is not in GMT" + ); + + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + }); + }); +}); From 091ee6eceec03c4975e545d0751028d2ffef4344 Mon Sep 17 00:00:00 2001 From: Namit Nathwani Date: Wed, 11 Jun 2025 15:09:03 +0530 Subject: [PATCH 2/6] update: rename test --- test/session/session.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/session/session.test.js b/test/session/session.test.js index d75fd45..b2a2e86 100644 --- a/test/session/session.test.js +++ b/test/session/session.test.js @@ -103,7 +103,7 @@ const createNewSession = async ( describe(`sessionTests: ${printPath("[test/session/session.test.js]")}`, function () { describe("Cookie checks", function () { - it("create new session", async function () { + it("access and refresh tokens set correctly on new session", async function () { const connectionURI = await createCoreApplication(); supertokens.init({ supertokens: { From cb9d229876ecdfacfd31fa23a55749221dc5f1b7 Mon Sep 17 00:00:00 2001 From: Namit Nathwani Date: Fri, 13 Jun 2025 11:54:44 +0530 Subject: [PATCH 3/6] test: different signup endpoint --- test/session/session.test.js | 75 +++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/test/session/session.test.js b/test/session/session.test.js index b2a2e86..e472d47 100644 --- a/test/session/session.test.js +++ b/test/session/session.test.js @@ -14,7 +14,7 @@ */ const { printPath, createCoreApplication } = require("../utils"); const assert = require("assert"); -const { recipesMock } = require("../../api-mock"); +const { recipesMock, request } = require("../../api-mock"); const { EmailPassword, Session, supertokens } = recipesMock; const SuperTokens = require("supertokens-node"); const { getMockStatus, initApp, fdiVersion, API_PORT } = require("../../build/api-mock/fetcher"); @@ -57,6 +57,54 @@ async function queryAPI({ method, path, input, headers, returnResponse, skipInit } } +const signUpOriginal = async (tenantId, email, password, session, userContext) => { + let response = await new Promise((resolve) => + request() + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: email, + }, + { + id: "password", + value: password, + }, + ], + tenantId, + session, + userContext, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + response = response.body; + + console.log(response); + + return { + ...response, + ...("user" in response + ? { + user: new UserClass(response.user), + } + : {}), + ...("recipeUserId" in response + ? { + recipeUserId: SuperTokens.convertToRecipeUserId(response.recipeUserId), + } + : {}), + }; +}; + const signUp = async (tenantId, email, password, session, userContext) => { const { response } = await queryAPI({ method: "post", @@ -101,6 +149,29 @@ const createNewSession = async ( return { session: deserializeSession(response), headers }; }; +const createNewSessionWithoutRequestResponse = async ( + tenantId, + recipeUserId, + accessTokenPayload, + sessionDataInDatabase, + disableAntiCsrf, + userContext +) => { + const { response, headers } = await queryAPI({ + method: "post", + path: "/test/session/createnewsessionwithoutrequestresponse", + input: { + tenantId, + recipeUserId: recipeUserId.getAsString(), + accessTokenPayload, + sessionDataInDatabase, + disableAntiCsrf, + userContext, + }, + }); + return { session: deserializeSession(response), headers }; +} + describe(`sessionTests: ${printPath("[test/session/session.test.js]")}`, function () { describe("Cookie checks", function () { it("access and refresh tokens set correctly on new session", async function () { @@ -117,7 +188,7 @@ describe(`sessionTests: ${printPath("[test/session/session.test.js]")}`, functio recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: "cookie" })], }); - const signUpResponse = await signUp("public", "test@example.com", "password123"); + const signUpResponse = await signUpOriginal("public", "test@example.com", "password123"); const epUser = signUpResponse.user; // Create a new session for the user, get headers from the response From e8621ea416675f19f65d8e2875ae46969cd7d7cf Mon Sep 17 00:00:00 2001 From: Namit Nathwani Date: Mon, 30 Jun 2025 18:13:29 +0530 Subject: [PATCH 4/6] update: make calls directly to API for signup --- api-mock/mocks/SessionMock.ts | 22 ------ test/session/session.test.js | 134 ++++------------------------------ 2 files changed, 16 insertions(+), 140 deletions(-) diff --git a/api-mock/mocks/SessionMock.ts b/api-mock/mocks/SessionMock.ts index f98023c..d9ce854 100644 --- a/api-mock/mocks/SessionMock.ts +++ b/api-mock/mocks/SessionMock.ts @@ -273,28 +273,6 @@ export const SessionMock: Partial = { }); return deserializeSession(response); }, - createNewSession: async ( - req, - res, - tenantId, - recipeUserId, - accessTokenPayload, - sessionDataInDatabase, - userContext - ) => { - const response = await queryAPI({ - method: "post", - path: "/test/session/createnewsession", - input: { - tenantId, - recipeUserId: recipeUserId.getAsString(), - accessTokenPayload, - sessionDataInDatabase, - userContext, - }, - }); - return deserializeSession(response); - }, getSessionWithoutRequestResponse: async ( accessToken: string, antiCsrfToken?: string, diff --git a/test/session/session.test.js b/test/session/session.test.js index e472d47..f77537e 100644 --- a/test/session/session.test.js +++ b/test/session/session.test.js @@ -17,47 +17,10 @@ const assert = require("assert"); const { recipesMock, request } = require("../../api-mock"); const { EmailPassword, Session, supertokens } = recipesMock; const SuperTokens = require("supertokens-node"); -const { getMockStatus, initApp, fdiVersion, API_PORT } = require("../../build/api-mock/fetcher"); -const { deserializeSession } = require("../../build/api-mock/mocks/SessionMock"); const setCookieParser = require("set-cookie-parser"); const { User: UserClass } = require("supertokens-node/lib/build/user"); -const fetch = require("cross-fetch"); -// Re-defined functions since these tests require response headers to be parsed -async function queryAPI({ method, path, input, headers, returnResponse, skipInit }) { - if (!skipInit && getMockStatus() === "NOT_READY") { - await initApp(); - } - try { - let response = await fetch(`http://localhost:${API_PORT}${path}`, { - method, - headers: { - "Content-Type": "application/json", - "fdi-version": fdiVersion, - ...headers, - }, - body: JSON.stringify(input), - }); - - if (returnResponse) { - return { response, headers: response.headers }; - } - - if (!response.ok) { - throw response; - } - - return { - response: await response.json().catch(() => undefined), - headers: response.headers, - }; - } catch (error) { - console.log(error); - throw await error.json().catch(() => undefined); - } -} - -const signUpOriginal = async (tenantId, email, password, session, userContext) => { +const signUp = async (tenantId, email, password, session, userContext) => { let response = await new Promise((resolve) => request() .post("/auth/signup") @@ -86,92 +49,25 @@ const signUpOriginal = async (tenantId, email, password, session, userContext) = }) ); - response = response.body; - - console.log(response); - - return { - ...response, - ...("user" in response - ? { - user: new UserClass(response.user), - } - : {}), - ...("recipeUserId" in response - ? { - recipeUserId: SuperTokens.convertToRecipeUserId(response.recipeUserId), - } - : {}), - }; -}; + const responseBody = response.body; -const signUp = async (tenantId, email, password, session, userContext) => { - const { response } = await queryAPI({ - method: "post", - path: "/test/emailpassword/signup", - input: { tenantId, email, password, session, userContext }, - }); return { - ...response, - ...("user" in response + headers: response.headers, + response: { + ...responseBody, + ...("user" in responseBody ? { - user: new UserClass(response.user), + user: new UserClass(responseBody.user), } : {}), - ...("recipeUserId" in response + ...("recipeUserId" in responseBody ? { - recipeUserId: SuperTokens.convertToRecipeUserId(response.recipeUserId), + recipeUserId: SuperTokens.convertToRecipeUserId(responseBody.recipeUserId), } : {}), - }; + }}; }; -const createNewSession = async ( - tenantId, - recipeUserId, - accessTokenPayload, - sessionDataInDatabase, - disableAntiCsrf, - userContext -) => { - const { response, headers } = await queryAPI({ - method: "post", - path: "/test/session/createnewsession", - input: { - tenantId, - recipeUserId: recipeUserId.getAsString(), - accessTokenPayload, - sessionDataInDatabase, - disableAntiCsrf, - userContext, - }, - }); - return { session: deserializeSession(response), headers }; -}; - -const createNewSessionWithoutRequestResponse = async ( - tenantId, - recipeUserId, - accessTokenPayload, - sessionDataInDatabase, - disableAntiCsrf, - userContext -) => { - const { response, headers } = await queryAPI({ - method: "post", - path: "/test/session/createnewsessionwithoutrequestresponse", - input: { - tenantId, - recipeUserId: recipeUserId.getAsString(), - accessTokenPayload, - sessionDataInDatabase, - disableAntiCsrf, - userContext, - }, - }); - return { session: deserializeSession(response), headers }; -} - describe(`sessionTests: ${printPath("[test/session/session.test.js]")}`, function () { describe("Cookie checks", function () { it("access and refresh tokens set correctly on new session", async function () { @@ -188,13 +84,15 @@ describe(`sessionTests: ${printPath("[test/session/session.test.js]")}`, functio recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: "cookie" })], }); - const signUpResponse = await signUpOriginal("public", "test@example.com", "password123"); - const epUser = signUpResponse.user; + const { response, headers } = await signUp("public", "test@example.com", "password123"); + const epUser = response.user; // Create a new session for the user, get headers from the response - const { session, headers } = await createNewSession("public", epUser.loginMethods[0].recipeUserId); + const session = await Session.createNewSessionWithoutRequestResponse("public", epUser.loginMethods[0].recipeUserId); + + let cookies = headers?.["set-cookie"]; + assert(cookies, "No cookies found in response headers"); - let cookies = headers.get("set-cookie"); if (!Array.isArray(cookies)) { cookies = [cookies]; } From 9a449a02df29e75b74b78651def499f01f4afc33 Mon Sep 17 00:00:00 2001 From: Namit Nathwani Date: Wed, 16 Jul 2025 12:12:55 +0530 Subject: [PATCH 5/6] fix: parse cookie strings directly --- test/session/session.test.js | 54 +++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/test/session/session.test.js b/test/session/session.test.js index f77537e..a20ee8a 100644 --- a/test/session/session.test.js +++ b/test/session/session.test.js @@ -42,7 +42,7 @@ const signUp = async (tenantId, email, password, session, userContext) => { .expect(200) .end((err, res) => { if (err) { - resolve(undefined); + reject(undefined); } else { resolve(res); } @@ -54,18 +54,19 @@ const signUp = async (tenantId, email, password, session, userContext) => { return { headers: response.headers, response: { - ...responseBody, - ...("user" in responseBody - ? { - user: new UserClass(responseBody.user), - } - : {}), - ...("recipeUserId" in responseBody - ? { - recipeUserId: SuperTokens.convertToRecipeUserId(responseBody.recipeUserId), - } - : {}), - }}; + ...responseBody, + ...("user" in responseBody + ? { + user: new UserClass(responseBody.user), + } + : {}), + ...("recipeUserId" in responseBody + ? { + recipeUserId: SuperTokens.convertToRecipeUserId(responseBody.recipeUserId), + } + : {}), + } + }; }; describe(`sessionTests: ${printPath("[test/session/session.test.js]")}`, function () { @@ -102,12 +103,27 @@ describe(`sessionTests: ${printPath("[test/session/session.test.js]")}`, functio .flat() // Ensure we have a flat array of cookies // Split cookie strings into arrays .map((cookieStr) => setCookieParser.splitCookiesString(cookieStr)) - .flat() // Since we have an array of arrays now - // `parse` the cookies + .flat(); // Since we have an array of arrays now + + console.log(cookies); + + cookies.forEach((cookieStr) => { + if (cookieStr.startsWith("sAccessToken=") || cookieStr.startsWith("sRefreshToken=")) { + cookieStr.split("; ").forEach((part) => { + if (part.startsWith("Expires=")) { + assert(part.endsWith("GMT"), "Cookie expiry is not in GMT format"); + } + }); + } + }); + + const parsedCookies = cookies .map(setCookieParser.parseString); - const accessTokenCookie = cookies.find((info) => (info?.key ?? info?.name) == "sAccessToken"); - const refreshTokenCookie = cookies.find((info) => (info?.key ?? info?.name) === "sRefreshToken"); + const accessTokenCookie = parsedCookies.find((info) => (info?.key ?? info?.name) == "sAccessToken"); + const refreshTokenCookie = parsedCookies.find((info) => (info?.key ?? info?.name) === "sRefreshToken"); + + console.log(new Date(accessTokenCookie.expires).getTimezoneOffset()) // Ensure cookies are set and with GMT timezones // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Date - Date headers are always GMT @@ -115,14 +131,14 @@ describe(`sessionTests: ${printPath("[test/session/session.test.js]")}`, functio assert(accessTokenCookie, "Access token cookie not found"); assert(accessTokenCookie.expires, "Access token cookie expiry not set"); assert( - new Date(accessTokenCookie.expires).toUTCString().endsWith("GMT"), + new Date(accessTokenCookie.expires).getTimezoneOffset() === 0, "Access token cookie expiry is not in GMT" ); assert(refreshTokenCookie, "Refresh token cookie not found"); assert(refreshTokenCookie.expires, "Refresh token cookie expiry not set"); assert( - new Date(refreshTokenCookie.expires).toUTCString().endsWith("GMT"), + new Date(refreshTokenCookie.expires).getTimezoneOffset() === 0, "Refresh token cookie expiry is not in GMT" ); From bbd028fcdb3e58a197f73c9521e1edc930034309 Mon Sep 17 00:00:00 2001 From: Namit Nathwani Date: Mon, 11 Aug 2025 12:46:20 +0530 Subject: [PATCH 6/6] fix: remove debug logs and invalid check - `getTimezoneOffset` works on local time, not ideal for a test --- test/session/session.test.js | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/test/session/session.test.js b/test/session/session.test.js index a20ee8a..be90c14 100644 --- a/test/session/session.test.js +++ b/test/session/session.test.js @@ -105,13 +105,14 @@ describe(`sessionTests: ${printPath("[test/session/session.test.js]")}`, functio .map((cookieStr) => setCookieParser.splitCookiesString(cookieStr)) .flat(); // Since we have an array of arrays now - console.log(cookies); - + // Ensure cookies are set and with GMT timezones + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Date - Date headers are always GMT + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#expiresdate - Invalid cookies become session cookies cookies.forEach((cookieStr) => { if (cookieStr.startsWith("sAccessToken=") || cookieStr.startsWith("sRefreshToken=")) { cookieStr.split("; ").forEach((part) => { if (part.startsWith("Expires=")) { - assert(part.endsWith("GMT"), "Cookie expiry is not in GMT format"); + assert(part.endsWith("GMT"), "Cookie expiry is not in GMT"); } }); } @@ -123,24 +124,11 @@ describe(`sessionTests: ${printPath("[test/session/session.test.js]")}`, functio const accessTokenCookie = parsedCookies.find((info) => (info?.key ?? info?.name) == "sAccessToken"); const refreshTokenCookie = parsedCookies.find((info) => (info?.key ?? info?.name) === "sRefreshToken"); - console.log(new Date(accessTokenCookie.expires).getTimezoneOffset()) - - // Ensure cookies are set and with GMT timezones - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Date - Date headers are always GMT - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#expiresdate - Invalid cookies become session cookies assert(accessTokenCookie, "Access token cookie not found"); assert(accessTokenCookie.expires, "Access token cookie expiry not set"); - assert( - new Date(accessTokenCookie.expires).getTimezoneOffset() === 0, - "Access token cookie expiry is not in GMT" - ); assert(refreshTokenCookie, "Refresh token cookie not found"); assert(refreshTokenCookie.expires, "Refresh token cookie expiry not set"); - assert( - new Date(refreshTokenCookie.expires).getTimezoneOffset() === 0, - "Refresh token cookie expiry is not in GMT" - ); assert(session.getUserId() === session.getRecipeUserId().getAsString()); });