import * as Hapi from "@hapi/hapi"; import HapiBasic from "@hapi/basic"; import NextAuthPlugin from "."; describe("plugin option validation", () => { let server; beforeEach(async () => { server = new Hapi.Server(); }); it("should throw when options contain no next auth adapter", async () => { expect(server.register(NextAuthPlugin)).rejects.toThrow(); }); }); describe("plugin runtime", () => { let server; const user = { id: "abc", email: "abc@abc.abc" }; const session = { id: "zyx", userId: "abc", expires: Date.now(), sessionToken: "foo", accessToken: "bar", }; const start = async (mock) => { await server.register(HapiBasic); await server.register({ plugin: NextAuthPlugin, options: { nextAuthAdapterFactory: () => mock, }, }); await server.start(); return server; }; beforeEach(async () => { server = new Hapi.Server({ port: 0 }); }); afterEach(async () => { await server.stop(); }); it("createUser", async () => { const createUser = jest.fn(() => user); const profile = { email: "abc@abc.abc" }; await start({ createUser }); const { statusCode, result } = await server.inject({ method: "post", url: "/api/nextauth/createUser", payload: profile, }); expect(statusCode).toBe(200); expect(createUser).toHaveBeenCalledWith(profile); expect(result).toStrictEqual(user); }); it("createUser fails with invalid payload", async () => { const createUser = jest.fn(() => user); const profile = { name: "name" }; await start({ createUser }); const { statusCode } = await server.inject({ method: "post", url: "/api/nextauth/createUser", payload: profile, }); expect(statusCode).toBe(400); }); it("getUser", async () => { const getUser = jest.fn(() => user); await start({ getUser }); const { statusCode, result } = await server.inject({ method: "get", url: "/api/nextauth/getUser/abc", }); expect(statusCode).toBe(200); expect(getUser).toHaveBeenCalledWith("abc"); expect(result).toBe(user); }); it("getUserByEmail", async () => { const getUserByEmail = jest.fn(() => user); await start({ getUserByEmail }); const { statusCode, result } = await server.inject({ method: "get", url: "/api/nextauth/getUserByEmail/abc@abc.abc", }); expect(statusCode).toBe(200); expect(getUserByEmail).toHaveBeenCalledWith("abc@abc.abc"); expect(result).toBe(user); }); it("getUserByEmail fails with invalid email", async () => { const getUserByEmail = jest.fn(() => user); await start({ getUserByEmail }); const { statusCode } = await server.inject({ method: "get", url: "/api/nextauth/getUserByEmail/notanemail@foo", }); expect(statusCode).toBe(400); }); it("getUserByProviderAccountId", async () => { const getUserByProviderAccountId = jest.fn(() => user); await start({ getUserByProviderAccountId }); const { statusCode, result } = await server.inject({ method: "get", url: "/api/nextauth/getUserByProviderAccountId/foo/bar", }); expect(statusCode).toBe(200); expect(getUserByProviderAccountId).toHaveBeenCalledWith("foo", "bar"); expect(result).toBe(user); }); it("updateUser", async () => { const updateUser = jest.fn(() => user); await start({ updateUser }); const { statusCode, result } = await server.inject({ method: "put", url: "/api/nextauth/updateUser", payload: user, }); expect(statusCode).toBe(200); expect(updateUser).toHaveBeenCalledWith(user); expect(result).toStrictEqual(user); }); it("updateUser fails with invalid payload", async () => { const updateUser = jest.fn(() => user); await start({ updateUser }); const { statusCode } = await server.inject({ method: "put", url: "/api/nextauth/updateUser", payload: { // id not specified email: "abc@abc.abc", }, }); expect(statusCode).toBe(400); }); it("linkUser", async () => { const linkAccount = jest.fn(() => undefined); const args = { userId: "abc", providerId: "foo", providerType: "something", providerAccountId: "bar", refreshToken: "refreshToken", accessToken: "accessToken", accessTokenExpires: 10, }; await start({ linkAccount }); const { statusCode } = await server.inject({ method: "put", url: "/api/nextauth/linkAccount", payload: args, }); expect(statusCode).toBe(204); expect(linkAccount.mock.calls.length).toBe(1); }); it("createSession", async () => { const createSession = jest.fn(() => session); await start({ createSession }); const { statusCode, result } = await server.inject({ method: "post", url: "/api/nextauth/createSession", payload: user, }); expect(statusCode).toBe(200); expect(createSession).toHaveBeenCalledWith(user); expect(result).toStrictEqual(session); }); it("getSession", async () => { const getSession = jest.fn(() => session); await start({ getSession }); const { statusCode, result } = await server.inject({ method: "get", url: "/api/nextauth/getSession/xyz", }); expect(statusCode).toBe(200); expect(getSession).toHaveBeenCalledWith("xyz"); expect(result).toBe(session); }); it("updateSession", async () => { const updateSession = jest.fn(() => session); await start({ updateSession }); const { statusCode, result } = await server.inject({ method: "put", url: "/api/nextauth/updateSession", payload: session, }); expect(statusCode).toBe(200); expect(updateSession).toHaveBeenCalledWith( { ...session, expires: new Date(session.expires), }, false ); expect(result).toStrictEqual(session); }); it("updateSession - force", async () => { const updateSession = jest.fn(() => session); await start({ updateSession }); const { statusCode, result } = await server.inject({ method: "put", url: "/api/nextauth/updateSession?force=true", payload: session, }); expect(statusCode).toBe(200); expect(updateSession).toHaveBeenCalledWith( { ...session, expires: new Date(session.expires), }, true ); expect(result).toStrictEqual(session); }); it("deleteSession", async () => { const deleteSession = jest.fn(() => undefined); await start({ deleteSession }); const { statusCode } = await server.inject({ method: "delete", url: "/api/nextauth/deleteSession/xyz", }); expect(statusCode).toBe(204); expect(deleteSession).toHaveBeenCalledWith("xyz"); }); }); describe("plugin authentication", () => { const user = { id: "abc", email: "abc@abc.abc" }; const sharedSecret = "secret"; let server; const start = async (mock) => { await server.register(HapiBasic); await server.register({ plugin: NextAuthPlugin, options: { nextAuthAdapterFactory: () => mock, sharedSecret, }, }); await server.start(); return server; }; const basicHeader = (username, password) => "Basic " + Buffer.from(username + ":" + password, "utf8").toString("base64"); beforeEach(async () => { server = new Hapi.Server({ port: 0 }); }); afterEach(async () => { await server.stop(); }); it("getUser - no auth header fails", async () => { const getUser = jest.fn(() => user); await start({ getUser }); const { statusCode } = await server.inject({ method: "get", url: "/api/nextauth/getUser/abc", }); expect(statusCode).toBe(401); expect(getUser).toHaveBeenCalledTimes(0); }); it("getUser - with auth header suceeds", async () => { const getUser = jest.fn(() => user); await start({ getUser }); const { statusCode, result } = await server.inject({ method: "get", url: "/api/nextauth/getUser/abc", headers: { authorization: basicHeader(sharedSecret, ""), }, }); expect(statusCode).toBe(200); expect(getUser).toHaveBeenCalledWith("abc"); expect(result).toBe(user); }); it("getUser - with invalid credentials fails", async () => { const getUser = jest.fn(() => user); await start({ getUser }); const { statusCode } = await server.inject({ method: "get", url: "/api/nextauth/getUser/abc", headers: { authorization: basicHeader("wrong secret", ""), }, }); expect(statusCode).toBe(401); expect(getUser).toHaveBeenCalledTimes(0); }); it("getUser - with secret in password field fails", async () => { const getUser = jest.fn(() => user); await start({ getUser }); const { statusCode } = await server.inject({ method: "get", url: "/api/nextauth/getUser/abc", headers: { authorization: basicHeader("", "sharedSecret"), }, }); expect(statusCode).toBe(401); expect(getUser).toHaveBeenCalledTimes(0); }); });