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..d9ce854 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); } diff --git a/test/session/session.test.js b/test/session/session.test.js new file mode 100644 index 0000000..a20ee8a --- /dev/null +++ b/test/session/session.test.js @@ -0,0 +1,148 @@ +/* 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, request } = require("../../api-mock"); +const { EmailPassword, Session, supertokens } = recipesMock; +const SuperTokens = require("supertokens-node"); +const setCookieParser = require("set-cookie-parser"); +const { User: UserClass } = require("supertokens-node/lib/build/user"); + +const signUp = 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) { + reject(undefined); + } else { + resolve(res); + } + }) + ); + + const responseBody = response.body; + + return { + headers: response.headers, + response: { + ...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 () { + describe("Cookie checks", function () { + it("access and refresh tokens set correctly on 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 { 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 = await Session.createNewSessionWithoutRequestResponse("public", epUser.loginMethods[0].recipeUserId); + + let cookies = headers?.["set-cookie"]; + assert(cookies, "No cookies found in response headers"); + + 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 + + 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 = 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()); + }); + }); +});