diff --git a/packages/core/src/api/credentials.ts b/packages/core/src/api/credentials.ts index c279370a..99c16d42 100644 --- a/packages/core/src/api/credentials.ts +++ b/packages/core/src/api/credentials.ts @@ -60,7 +60,7 @@ export const signInCredentials = async ({ redirectURL: null, error: { code, message }, toResponse: () => { - return Response.json({ success: false, redirectURL: null, error: { code, message } }, { headers, status: 401 }) + return Response.json({ success: false, redirectURL: null }, { headers, status: 401 }) }, } if (error instanceof AuthValidationError) { diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts index 84d8211f..08ce4fc0 100644 --- a/packages/core/src/client/client.ts +++ b/packages/core/src/client/client.ts @@ -14,7 +14,6 @@ import type { SignOutReturn, UpdateSessionReturn, SignInCredentialsReturn, - SignInAPIReturn, SignInCredentialsOptions, } from "@/@types/index.ts" @@ -73,7 +72,7 @@ export const createAuthClient = (options: AuthC redirect: false, }, }) - const json = (await response.json()) as SignInAPIReturn + const json = await response.json() if ((options?.redirect ?? true) && typeof window !== "undefined" && json?.signInURL) { window.location.assign(json.signInURL) } diff --git a/packages/core/test/client/client.test.ts b/packages/core/test/client/client.test.ts index ba727e85..3e53f469 100644 --- a/packages/core/test/client/client.test.ts +++ b/packages/core/test/client/client.test.ts @@ -17,6 +17,11 @@ const createJSONResponse = (body: unknown, status = 200) => { }) } +beforeEach(() => { + vi.clearAllMocks() + vi.unstubAllGlobals() +}) + describe("createAuthClient", () => { beforeEach(() => { vi.clearAllMocks() @@ -54,7 +59,22 @@ describe("createAuthClient", () => { }) }) - test("getSession returns valid session", async () => { + test("infer baseURL from window.location.origin in browser environment", () => { + vi.stubGlobal("window", { location: { origin: "https://example.com" } }) + createClientMock.mockReturnValue({ + get: vi.fn(), + post: vi.fn(), + }) + + createAuthClient({}) + expect(createClientMock).toHaveBeenCalledWith({ + baseURL: "https://example.com", + cache: "no-store", + credentials: "include", + }) + }) + + test("getSession with valid session", async () => { const get = vi.fn().mockResolvedValue( createJSONResponse({ success: true, @@ -74,7 +94,27 @@ describe("createAuthClient", () => { expect(session).toEqual({ user: { id: "user_1" } }) }) - test("getSession returns null for non-ok response", async () => { + test("getSession with no session", async () => { + const get = vi.fn().mockResolvedValue( + createJSONResponse({ + success: false, + session: null, + }) + ) + + createClientMock.mockReturnValue({ + get, + post: vi.fn(), + }) + + const client = createAuthClient({ baseURL: "https://example.com" }) + const session = await client.getSession() + + expect(get).toHaveBeenCalledWith("/session") + expect(session).toBeNull() + }) + + test("getSession with 401 status code", async () => { const get = vi.fn().mockResolvedValue(createJSONResponse({}, 401)) createClientMock.mockReturnValue({ @@ -107,6 +147,7 @@ describe("createAuthClient", () => { }) test("signIn with redirect option", async () => { + vi.stubGlobal("window", { location: { assign: vi.fn() } }) const get = vi.fn().mockResolvedValue(createJSONResponse({ signInURL: "https://example.com/oauth" })) createClientMock.mockReturnValue({ @@ -114,18 +155,202 @@ describe("createAuthClient", () => { post: vi.fn(), }) const client = createAuthClient({ baseURL: "https://example.com" }) - const response = await client.signIn("github", { redirect: false, redirectTo: "/dashboard" }) + const response = await client.signIn("github", { redirect: true, redirectTo: "/dashboard" }) expect(get).toHaveBeenCalledWith("/signIn/:oauth", { params: { oauth: "github" }, searchParams: { + // The redirect is set to false in the request to prevent automatic + // redirection by server response by 302 status code. redirect: false, redirectTo: "/dashboard", }, }) + expect(window.location.assign).toHaveBeenCalledWith("https://example.com/oauth") expect(response).toEqual({ signInURL: "https://example.com/oauth" }) }) + test("signInCredentials", async () => { + const post = vi.fn().mockResolvedValue( + createJSONResponse({ + success: true, + redirectURL: "/", + }) + ) + + createClientMock.mockReturnValue({ + get: vi.fn(), + post, + }) + + const client = createAuthClient({ baseURL: "https://example.com" }) + const response = await client.signInCredentials({ payload: { username: "user", password: "pass" } }) + + expect(post).toHaveBeenCalledWith("/signIn/credentials", { + body: { username: "user", password: "pass" }, + searchParams: { + redirectTo: undefined, + }, + }) + expect(response).toEqual({ success: true, redirectURL: "/" }) + }) + + test("signInCredentials with redirectTo option", async () => { + const post = vi.fn().mockResolvedValue( + createJSONResponse({ + success: true, + redirectURL: "/dashboard", + }) + ) + createClientMock.mockReturnValue({ + get: vi.fn(), + post, + }) + const client = createAuthClient({ baseURL: "https://example.com" }) + const response = await client.signInCredentials({ + payload: { username: "user", password: "pass" }, + redirectTo: "/dashboard", + }) + + expect(post).toHaveBeenCalledWith("/signIn/credentials", { + body: { username: "user", password: "pass" }, + searchParams: { + redirectTo: "/dashboard", + }, + }) + expect(response).toEqual({ success: true, redirectURL: "/dashboard" }) + }) + + test("signInCredentials with invalid credentials", async () => { + const post = vi.fn().mockResolvedValue( + createJSONResponse( + { + success: false, + redirectURL: null, + }, + 401 + ) + ) + + createClientMock.mockReturnValue({ + get: vi.fn(), + post, + }) + + const client = createAuthClient({ baseURL: "https://example.com" }) + const response = await client.signInCredentials({ payload: { username: "user", password: "wrong_pass" } }) + + expect(post).toHaveBeenCalledWith("/signIn/credentials", { + body: { username: "user", password: "wrong_pass" }, + searchParams: { + redirectTo: undefined, + }, + }) + expect(response).toEqual({ success: false, redirectURL: null }) + }) + + test("updateSession without csrf token", async () => { + const get = vi.fn().mockResolvedValue(createJSONResponse({}, 500)) + const patch = vi.fn() + + createClientMock.mockReturnValue({ + get, + patch, + }) + + const client = createAuthClient({ baseURL: "https://example.com" }) + const response = await client.updateSession({ + session: { user: { name: "Alice" }, expires: new Date().toISOString() }, + }) + + expect(get).toHaveBeenCalledWith("/csrfToken") + expect(patch).not.toHaveBeenCalled() + expect(response).toEqual({ success: false, session: null }) + }) + + test("updateSession with valid session", async () => { + const get = vi.fn().mockResolvedValue(createJSONResponse({ csrfToken: "csrf_token_1" })) + + const patch = vi.fn().mockResolvedValue( + createJSONResponse({ + success: true, + session: { user: { id: "user_1", name: "Alice" } }, + }) + ) + + createClientMock.mockReturnValue({ + get, + patch, + }) + + const client = createAuthClient({ baseURL: "https://example.com" }) + const expires = new Date(Date.now() + 60 * 60 * 1000) + const response = await client.updateSession({ + session: { user: { name: "Alice" }, expires: expires.toISOString() }, + }) + + expect(patch).toHaveBeenCalledWith("/session", { + body: { + user: { name: "Alice" }, + expires, + }, + headers: { + "X-CSRF-Token": "csrf_token_1", + }, + }) + expect(response).toEqual({ success: true, session: { user: { id: "user_1", name: "Alice" } } }) + }) + + test("updateSession with no session", async () => { + const get = vi.fn().mockResolvedValue(createJSONResponse({ csrfToken: "csrf_token_1" })) + const patch = vi.fn() + + createClientMock.mockReturnValue({ + get, + patch, + }) + + const client = createAuthClient({ baseURL: "https://example.com" }) + const response = await client.updateSession({ session: {} }) + expect(response).toEqual({ success: false, session: null }) + }) + + test("updateSession with redirectTo option", async () => { + const get = vi.fn().mockResolvedValue(createJSONResponse({ csrfToken: "csrf_token_1" })) + + const patch = vi.fn().mockResolvedValue( + createJSONResponse({ + success: true, + session: { user: { id: "user_1", name: "Alice" } }, + redirectURL: "/dashboard", + }) + ) + + createClientMock.mockReturnValue({ + get, + patch, + }) + + const client = createAuthClient({ baseURL: "https://example.com" }) + const expires = new Date(Math.floor(Date.now() / 1000) + 60 * 60 * 1000) + const response = await client.updateSession({ + session: { user: { name: "Alice" }, expires: expires.toISOString() }, + redirect: true, + redirectTo: "/dashboard", + }) + + expect(patch).toHaveBeenCalledWith("/session", { + body: { + user: { name: "Alice" }, + expires, + }, + headers: { + "X-CSRF-Token": "csrf_token_1", + }, + }) + expect(response).toEqual({ success: true, session: { user: { id: "user_1", name: "Alice" } }, redirectURL: "/dashboard" }) + }) + test("signOut", async () => { const get = vi.fn().mockResolvedValue(createJSONResponse({ csrfToken: "csrf_token_1" })) const post = vi.fn().mockResolvedValue(createJSONResponse({ redirect: true, url: "/logout" }, 202))