diff --git a/docs/app/components/content/landing/hero.yml b/docs/app/components/content/landing/hero.yml index 04ea095..41c776b 100644 --- a/docs/app/components/content/landing/hero.yml +++ b/docs/app/components/content/landing/hero.yml @@ -34,11 +34,11 @@ tabs: - name: pages/login.vue code: | diff --git a/docs/content/1.getting-started/3.client-setup.md b/docs/content/1.getting-started/3.client-setup.md index cc7cffa..f2a8e56 100644 --- a/docs/content/1.getting-started/3.client-setup.md +++ b/docs/content/1.getting-started/3.client-setup.md @@ -13,7 +13,8 @@ Set up the client auth config for @onmax/nuxt-better-auth. - Object syntax: `defineClientAuth({})` or function syntax: `defineClientAuth(({ siteUrl }) => ({}))` - Add client plugin equivalents for every server plugin (e.g. `adminClient()` from `better-auth/client/plugins`) - The module calls the factory with the correct `baseURL` at runtime -- `useUserSession()` is auto-imported and provides `user`, `session`, `loggedIn`, `ready`, `signIn`, `signUp`, `signOut` +- `useUserSession()` is auto-imported and provides `user`, `session`, `loggedIn`, `ready`, `signOut` +- `useSignIn()`, `useSignUp()`, and `useAuthClient()` are auto-imported for auth actions and direct client access ``` :: @@ -80,10 +81,10 @@ export default defineClientAuth({ diff --git a/test/cases/pinia-setup-store/app/stores/full-auth.ts b/test/cases/pinia-setup-store/app/stores/full-auth.ts index 2f6f7b1..1491410 100644 --- a/test/cases/pinia-setup-store/app/stores/full-auth.ts +++ b/test/cases/pinia-setup-store/app/stores/full-auth.ts @@ -1,7 +1,5 @@ import { defineStore } from 'pinia' export const useFullAuthStore = defineStore('full-auth', () => { - const { client: authClient, ...auth } = useUserSession() - - return { authClient, ...auth } + return useUserSession() }) diff --git a/test/exports.test.ts b/test/exports.test.ts index f618817..cf34346 100644 --- a/test/exports.test.ts +++ b/test/exports.test.ts @@ -35,6 +35,7 @@ describe('exports-snapshot', async () => { './composables': { useAction: 'function', useAuthAsyncData: 'function', + useAuthClient: 'function', useAuthClientAction: 'function', useAuthRequestFetch: 'function', useSignIn: 'function', diff --git a/test/exports/module.yaml b/test/exports/module.yaml index 413bc15..af7f04f 100644 --- a/test/exports/module.yaml +++ b/test/exports/module.yaml @@ -5,6 +5,7 @@ ./composables: useAction: function useAuthAsyncData: function + useAuthClient: function useAuthClientAction: function useAuthRequestFetch: function useSignIn: function diff --git a/test/pinia-setup-store.test.ts b/test/pinia-setup-store.test.ts index 48a159d..66b4940 100644 --- a/test/pinia-setup-store.test.ts +++ b/test/pinia-setup-store.test.ts @@ -7,7 +7,7 @@ describe('pinia setup store auth regression', async () => { rootDir: fileURLToPath(new URL('./cases/pinia-setup-store', import.meta.url)), }) - it('renders setup stores that expose auth state and client facades', async () => { + it('renders setup stores that expose auth state directly', async () => { await expect($fetch('/')).resolves.toContain('Pinia Auth Store') }) }) diff --git a/test/use-auth-client-action.test.ts b/test/use-auth-client-action.test.ts index 919059d..b6603a4 100644 --- a/test/use-auth-client-action.test.ts +++ b/test/use-auth-client-action.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -let sessionMock: any +let clientMock: any vi.mock('#imports', async () => { const vue = await import('vue') @@ -10,8 +10,8 @@ vi.mock('#imports', async () => { } }) -vi.mock('../src/runtime/app/composables/useUserSession', () => ({ - useUserSession: () => sessionMock, +vi.mock('../src/runtime/app/composables/useAuthClient', () => ({ + useAuthClient: () => clientMock, })) async function loadUseAuthClientAction() { @@ -23,9 +23,7 @@ async function loadUseAuthClientAction() { describe('useAuthClientAction', () => { it('executes selected top-level client method', async () => { const checkout = vi.fn(async () => ({ ok: true })) - sessionMock = { - client: { checkout }, - } + clientMock = { checkout } const useAuthClientAction = await loadUseAuthClientAction() const action = useAuthClientAction(client => client.checkout as any) @@ -38,9 +36,7 @@ describe('useAuthClientAction', () => { it('executes selected nested client method', async () => { const portal = vi.fn(async () => ({ opened: true })) - sessionMock = { - client: { customer: { portal } }, - } + clientMock = { customer: { portal } } const useAuthClientAction = await loadUseAuthClientAction() const action = useAuthClientAction(client => client.customer.portal as any) @@ -51,7 +47,7 @@ describe('useAuthClientAction', () => { }) it('sets normalized error when client is unavailable', async () => { - sessionMock = { client: null } + clientMock = null const useAuthClientAction = await loadUseAuthClientAction() const action = useAuthClientAction(client => client.checkout as any) @@ -62,7 +58,7 @@ describe('useAuthClientAction', () => { }) it('sets normalized error when selector does not resolve to a function', async () => { - sessionMock = { client: { customer: {} } } + clientMock = { customer: {} } const useAuthClientAction = await loadUseAuthClientAction() const action = useAuthClientAction(client => (client as any).customer.portal) diff --git a/test/use-auth-client.test.ts b/test/use-auth-client.test.ts new file mode 100644 index 0000000..00784ee --- /dev/null +++ b/test/use-auth-client.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it, vi } from 'vitest' +import { isReactive, isRef } from 'vue' + +const runtimeConfig = { + public: { + siteUrl: 'http://localhost:3000', + }, +} +const requestURL = { + origin: 'http://request-origin.test', +} +const rawClient = { + signIn: { + email: vi.fn(async () => ({ ok: true })), + }, + sendVerificationEmail: vi.fn(async () => ({ ok: true })), +} +const createAppAuthClient = vi.fn(() => rawClient) + +vi.mock('#auth/client', () => ({ + default: createAppAuthClient, +})) + +vi.mock('#imports', () => ({ + useRequestURL: () => requestURL, + useRuntimeConfig: () => runtimeConfig, +})) + +function setRuntimeFlags(flags: { client: boolean, server: boolean }) { + const state = globalThis as { __NUXT_BETTER_AUTH_TEST_FLAGS__?: { client: boolean, server: boolean } } + state.__NUXT_BETTER_AUTH_TEST_FLAGS__ = flags +} + +async function loadUseAuthClient() { + vi.resetModules() + const mod = await import('../src/runtime/app/composables/useAuthClient') + return mod.useAuthClient +} + +describe('useAuthClient', () => { + it('returns null on server runtime', async () => { + setRuntimeFlags({ client: false, server: true }) + + const useAuthClient = await loadUseAuthClient() + + expect(useAuthClient()).toBeNull() + expect(createAppAuthClient).not.toHaveBeenCalled() + }) + + it('returns a Vue-safe client facade on client runtime', async () => { + setRuntimeFlags({ client: true, server: false }) + + const useAuthClient = await loadUseAuthClient() + const client = useAuthClient() + + expect(createAppAuthClient).toHaveBeenCalledWith('http://localhost:3000') + expect(client).not.toBeNull() + expect(isRef(client!.signIn.email)).toBe(false) + expect(isReactive(client!.signIn.email)).toBe(false) + await expect(client!.signIn.email({ email: 'user@example.com', password: 'password' })).resolves.toEqual({ ok: true }) + }) +}) diff --git a/test/use-signin.test.ts b/test/use-signin.test.ts index 63addd3..a63cf4b 100644 --- a/test/use-signin.test.ts +++ b/test/use-signin.test.ts @@ -13,6 +13,7 @@ vi.mock('#imports', async () => { vi.mock('../src/runtime/app/composables/useUserSession', () => ({ useUserSession: () => sessionMock, + useAuthActionNamespaces: () => sessionMock, })) async function loadUseSignIn() { diff --git a/test/use-signup.test.ts b/test/use-signup.test.ts index 40d941f..73b89b0 100644 --- a/test/use-signup.test.ts +++ b/test/use-signup.test.ts @@ -13,6 +13,7 @@ vi.mock('#imports', async () => { vi.mock('../src/runtime/app/composables/useUserSession', () => ({ useUserSession: () => sessionMock, + useAuthActionNamespaces: () => sessionMock, })) async function loadUseSignUp() { diff --git a/test/use-user-session-state.test.ts b/test/use-user-session-state.test.ts index c113d15..aa7ca18 100644 --- a/test/use-user-session-state.test.ts +++ b/test/use-user-session-state.test.ts @@ -10,14 +10,6 @@ vi.mock('../src/runtime/app/composables/useUserSession', () => ({ useUserSession: authMock.useUserSession, })) -function createRecursiveProxy(): any { - return new Proxy({}, { - get() { - return createRecursiveProxy() - }, - }) -} - function expectPiniaInspectionSafe(value: unknown) { expect(() => isRef(value)).not.toThrow() expect(() => isReadonly(value)).not.toThrow() @@ -28,9 +20,6 @@ function expectPiniaInspectionSafe(value: unknown) { describe('useUserSessionState', () => { beforeEach(() => { authMock.useUserSession.mockReturnValue({ - client: createRecursiveProxy(), - signIn: createRecursiveProxy(), - signUp: createRecursiveProxy(), session: ref(null), user: ref(null), loggedIn: computed(() => false), diff --git a/test/use-user-session.test.ts b/test/use-user-session.test.ts index 8578c87..3e07c08 100644 --- a/test/use-user-session.test.ts +++ b/test/use-user-session.test.ts @@ -1,6 +1,6 @@ import { createPinia, defineStore, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { isReactive, isReadonly, isRef, reactive, ref, watch } from 'vue' +import { isReactive, isRef, ref, watch } from 'vue' interface SessionState { data: { session: Record, user: Record } | null @@ -100,6 +100,11 @@ async function loadUseUserSession() { return mod.useUserSession } +async function loadAuthComposables() { + vi.resetModules() + return import('../src/runtime/app/composables/useUserSession') +} + async function flushPromises() { await Promise.resolve() await Promise.resolve() @@ -135,16 +140,6 @@ function createDynamicAuthProxy(routes: Record = {}, calls: str }) } -function expectVueInspectionSafe(value: unknown) { - expect(() => isRef(value)).not.toThrow() - expect(() => isReadonly(value)).not.toThrow() - expect(() => isReactive(value)).not.toThrow() - expect(() => reactive({ value })).not.toThrow() - expect(isRef(value)).toBe(false) - expect(isReadonly(value)).toBe(false) - expect(isReactive(value)).toBe(false) -} - describe('useUserSession hydration bootstrap', () => { beforeEach(() => { state.clear() @@ -228,9 +223,9 @@ describe('useUserSession hydration bootstrap', () => { runtimeConfig.public.auth.session.skipHydratedSsrGetSession = true const useUserSession = await loadUseUserSession() - const auth = useUserSession() + useUserSession() - expect(auth.client).not.toBeNull() + expect(mockClient.useSession).toHaveBeenCalledOnce() }) it('bootstraps client session for prerendered/cached payloads', async () => { @@ -240,9 +235,9 @@ describe('useUserSession hydration bootstrap', () => { seedHydratedState() const useUserSession = await loadUseUserSession() - const auth = useUserSession() + useUserSession() - expect(auth.client).not.toBeNull() + expect(mockClient.useSession).toHaveBeenCalledOnce() }) it('defers ready reset until suspense resolves during prerender hydration empty snapshot', async () => { @@ -307,9 +302,9 @@ describe('useUserSession hydration bootstrap', () => { seedHydratedState() const useUserSession = await loadUseUserSession() - const auth = useUserSession() + useUserSession() - expect(auth.client).not.toBeNull() + expect(mockClient.useSession).toHaveBeenCalledOnce() }) it('reconciles hydrated SSR auth state before clearing it', async () => { @@ -462,32 +457,28 @@ describe('useUserSession hydration bootstrap', () => { expect(auth.ready.value).toBe(true) }) - it('exposes client as null on server runtime', async () => { + it('returns only store-safe session state and actions', async () => { setRuntimeFlags({ client: false, server: true }) const useUserSession = await loadUseUserSession() const auth = useUserSession() - expect(auth.client).toBeNull() + expect(Object.keys(auth).sort()).toEqual([ + 'fetchSession', + 'loggedIn', + 'ready', + 'session', + 'signOut', + 'updateUser', + 'user', + 'waitForSession', + ]) + expect('client' in auth).toBe(false) + expect('signIn' in auth).toBe(false) + expect('signUp' in auth).toBe(false) }) - it('allows SSR metadata introspection on signIn and signUp without throwing', async () => { - setRuntimeFlags({ client: false, server: true }) - - const useUserSession = await loadUseUserSession() - const auth = useUserSession() - - expect(isRef(auth.signIn as unknown)).toBe(false) - expect(isReactive(auth.signIn as unknown)).toBe(false) - expect(isRef(auth.signUp as unknown)).toBe(false) - expect(isReactive(auth.signUp as unknown)).toBe(false) - expect((auth.signIn as Record).__v_isRef).toBeUndefined() - expect((auth.signIn as Record).__v_isReactive).toBeUndefined() - expect((auth.signUp as Record).__v_isRef).toBeUndefined() - expect((auth.signUp as Record).__v_isReactive).toBeUndefined() - }) - - it('allows client-side metadata introspection on the Better Auth client facade', async () => { + it('keeps useUserSession safe for Pinia setup-store forwarding on client', async () => { const rawClient = createDynamicAuthProxy({ useSession: mockClient.useSession, getSession: mockClient.getSession, @@ -499,88 +490,35 @@ describe('useUserSession hydration bootstrap', () => { activeClient = rawClient const useUserSession = await loadUseUserSession() - const auth = useUserSession() - const secondAuth = useUserSession() - - expect(auth.client).not.toBe(rawClient) - expect(typeof auth.client).toBe('object') - expect(secondAuth.client).toBe(auth.client) - expect(secondAuth.client!.signIn).toBe(auth.client!.signIn) - expect(secondAuth.client!.signIn.email).toBe(auth.client!.signIn.email) - expect(typeof (auth.client as Record).sendVerificationEmail).toBe('function') - expectVueInspectionSafe(auth.client) - expectVueInspectionSafe(auth.client!.signIn) - expectVueInspectionSafe(auth.client!.signIn.email) - expectVueInspectionSafe(auth.client!.signUp) - expectVueInspectionSafe(auth.client!.signUp.email) - expectVueInspectionSafe((auth.client as Record).sendVerificationEmail) - expectVueInspectionSafe(auth.client!.admin.impersonateUser) - }) - - it('keeps forwarded useUserSession actions out of Pinia setup-store state classification on client', async () => { - const rawClient = createDynamicAuthProxy({ - useSession: mockClient.useSession, - getSession: mockClient.getSession, - signOut: mockClient.signOut, - signIn: createDynamicAuthProxy(), - signUp: createDynamicAuthProxy(), - $store: mockClient.$store, - }) - activeClient = rawClient - - const useUserSession = await loadUseUserSession() - const { user, client: authClient, fetchSession, ...rest } = useUserSession() - const store = { - user, - authClient, - fetchSession, - ...rest, - } + const store = { ...useUserSession() } const isStateLike = (value: unknown) => isRef(value) || isReactive(value) - expect(isStateLike(store.authClient)).toBe(false) - expect(isStateLike(store.signIn)).toBe(false) - expect(isStateLike(store.signUp)).toBe(false) - expect(typeof store.authClient).toBe('object') - expect(typeof (store.authClient as Record).sendVerificationEmail).toBe('function') - expect(isStateLike((store.authClient as Record).signIn)).toBe(false) - expect(isStateLike((store.authClient as Record).admin)).toBe(false) - expect(isStateLike((store.authClient as Record).sendVerificationEmail)).toBe(false) - expect(isStateLike((store.signIn as Record).email)).toBe(false) - expect(isStateLike((store.signUp as Record).email)).toBe(false) + expect('client' in store).toBe(false) + expect('signIn' in store).toBe(false) + expect('signUp' in store).toBe(false) + expect(isStateLike(store.signOut)).toBe(false) + expect(isStateLike(store.fetchSession)).toBe(false) }) - it('keeps forwarded authClient methods callable in an actual Pinia setup store', async () => { - const rawClient = createDynamicAuthProxy({ - useSession: mockClient.useSession, - getSession: mockClient.getSession, - signOut: mockClient.signOut, - signIn: createDynamicAuthProxy(), - signUp: createDynamicAuthProxy(), - $store: mockClient.$store, - }) - activeClient = rawClient + it('can return useUserSession directly from an actual Pinia setup store', async () => { setActivePinia(createPinia()) const useUserSession = await loadUseUserSession() - const useAuthStore = defineStore('auth-client-forwarding', () => { - const { client: authClient, ...auth } = useUserSession() - return { authClient, ...auth } - }) + const useAuthStore = defineStore('auth-session-forwarding', () => useUserSession()) const store = useAuthStore() - expect(typeof store.authClient).toBe('object') - expect(typeof (store.authClient as Record).sendVerificationEmail).toBe('function') - expect(isRef(store.authClient as unknown)).toBe(false) - expect(isReactive(store.authClient as unknown)).toBe(false) - expect(isReactive((store.authClient as Record).sendVerificationEmail)).toBe(false) + expect('client' in store).toBe(false) + expect('signIn' in store).toBe(false) + expect('signUp' in store).toBe(false) + expect(isReactive(store.signOut)).toBe(false) + expect(isReactive(store.fetchSession)).toBe(false) }) - it('allows server-side auth method reads but still rejects invocation', async () => { + it('allows server-side auth method reads through action namespaces but still rejects invocation', async () => { setRuntimeFlags({ client: false, server: true }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() const signInEmail = (auth.signIn as Record Promise>).email const signUpEmail = (auth.signUp as Record Promise>).email @@ -594,23 +532,18 @@ describe('useUserSession hydration bootstrap', () => { await expect(signUpEmail({ email: 'user@example.com', password: 'password', name: 'User' })).rejects.toThrow('signUp.email() can only be called on client-side') }) - it('keeps forwarded useUserSession actions out of Pinia setup-store state classification during SSR', async () => { + it('keeps useUserSession safe for Pinia setup-store forwarding during SSR', async () => { setRuntimeFlags({ client: false, server: true }) const useUserSession = await loadUseUserSession() - const { user, client: authClient, fetchSession, ...rest } = useUserSession() - const store = { - user, - authClient, - fetchSession, - ...rest, - } + const store = { ...useUserSession() } const isStateLike = (value: unknown) => isRef(value) || isReactive(value) - expect(isStateLike(store.signIn)).toBe(false) - expect(isStateLike(store.signUp)).toBe(false) - expect(isStateLike((store.signIn as Record).email)).toBe(false) - expect(isStateLike((store.signUp as Record).email)).toBe(false) + expect('client' in store).toBe(false) + expect('signIn' in store).toBe(false) + expect('signUp' in store).toBe(false) + expect(isStateLike(store.signOut)).toBe(false) + expect(isStateLike(store.fetchSession)).toBe(false) }) it('signIn uses auth.redirects.authenticated when no callback is provided', async () => { @@ -625,8 +558,8 @@ describe('useUserSession hydration bootstrap', () => { await opts?.onSuccess?.('ctx') }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signIn.email({ email: 'user@example.com', password: 'password' }) expect(navigateTo).toHaveBeenCalledWith('/app') @@ -645,8 +578,8 @@ describe('useUserSession hydration bootstrap', () => { await opts?.onSuccess?.('ctx') }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signIn.email({ email: 'user@example.com', password: 'password' }) expect(navigateTo).toHaveBeenCalledWith('/app/billing') @@ -665,8 +598,8 @@ describe('useUserSession hydration bootstrap', () => { await opts?.onSuccess?.('ctx') }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signIn.email({ email: 'user@example.com', password: 'password' }) expect(navigateTo).toHaveBeenCalledWith('/app') @@ -683,8 +616,8 @@ describe('useUserSession hydration bootstrap', () => { await opts?.onSuccess?.('ctx') }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signIn.email({ email: 'user@example.com', password: 'password' }) expect(navigateTo).not.toHaveBeenCalled() @@ -694,8 +627,8 @@ describe('useUserSession hydration bootstrap', () => { runtimeConfig.public.auth.redirects = { authenticated: '/app' } mockClient.signIn.social.mockResolvedValueOnce({ url: 'https://github.com/login/oauth/authorize', redirect: true }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signIn.social({ provider: 'github' } as never) @@ -709,8 +642,8 @@ describe('useUserSession hydration bootstrap', () => { requestURL.searchParams = new URLSearchParams({ redirect: '/app/billing' }) mockClient.signIn.social.mockResolvedValueOnce({ url: 'https://github.com/login/oauth/authorize', redirect: true }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signIn.social({ provider: 'github' } as never) @@ -722,8 +655,8 @@ describe('useUserSession hydration bootstrap', () => { requestURL.searchParams = new URLSearchParams({ redirect: 'https://evil.com/phish' }) mockClient.signIn.social.mockResolvedValueOnce({ url: 'https://github.com/login/oauth/authorize', redirect: true }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signIn.social({ provider: 'github' } as never) @@ -735,8 +668,8 @@ describe('useUserSession hydration bootstrap', () => { requestURL.searchParams = new URLSearchParams({ redirect: '/app/billing' }) mockClient.signIn.social.mockResolvedValueOnce({ url: 'https://github.com/login/oauth/authorize', redirect: true }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signIn.social({ provider: 'github', callbackURL: '/custom' } as never) @@ -749,8 +682,8 @@ describe('useUserSession hydration bootstrap', () => { await opts?.onSuccess?.('ctx') }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signIn.social({ provider: 'github' } as never, { onSuccess } as never) @@ -759,10 +692,10 @@ describe('useUserSession hydration bootstrap', () => { }) it('signIn.social with disableRedirect wraps explicit onSuccess with session sync', async () => { - let auth!: ReturnType>> + let sessionAuth!: ReturnType>> let sessionAtCallback: unknown const onSuccess = vi.fn(() => { - sessionAtCallback = auth.session.value + sessionAtCallback = sessionAuth.session.value }) mockClient.getSession.mockResolvedValueOnce({ data: { @@ -774,8 +707,9 @@ describe('useUserSession hydration bootstrap', () => { await opts?.onSuccess?.('ctx') }) - const useUserSession = await loadUseUserSession() - auth = useUserSession() + const { useAuthActionNamespaces, useUserSession } = await loadAuthComposables() + sessionAuth = useUserSession() + const auth = useAuthActionNamespaces() await auth.signIn.social({ provider: 'github', disableRedirect: true } as never, { onSuccess } as never) @@ -795,8 +729,8 @@ describe('useUserSession hydration bootstrap', () => { await opts?.onSuccess?.('ctx') }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signIn.social({ provider: 'github', disableRedirect: true } as never) @@ -811,8 +745,8 @@ describe('useUserSession hydration bootstrap', () => { await opts?.onSuccess?.('ctx') }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signUp.email({ email: 'user@example.com', password: 'password', name: 'User' }) @@ -831,8 +765,8 @@ describe('useUserSession hydration bootstrap', () => { await opts?.onSuccess?.('ctx') }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signUp.email({ email: 'user@example.com', password: 'password', name: 'User' }) expect(navigateTo).toHaveBeenCalledWith('/app') @@ -851,8 +785,8 @@ describe('useUserSession hydration bootstrap', () => { await opts?.onSuccess?.('ctx') }) - const useUserSession = await loadUseUserSession() - const auth = useUserSession() + const { useAuthActionNamespaces } = await loadAuthComposables() + const auth = useAuthActionNamespaces() await auth.signUp.email({ email: 'user@example.com', password: 'password', name: 'User' }) expect(navigateTo).toHaveBeenCalledWith('/welcome') diff --git a/test/vue-safe-auth-proxy.test.ts b/test/vue-safe-auth-proxy.test.ts index ed6da15..1b5ec4a 100644 --- a/test/vue-safe-auth-proxy.test.ts +++ b/test/vue-safe-auth-proxy.test.ts @@ -35,6 +35,18 @@ describe('createVueSafeAuthProxy', () => { expect(fetch).not.toHaveBeenCalled() }) + it('keeps nested namespaces out of Vue reactive wrapping', () => { + const client = createAuthClient({ + baseURL: 'http://localhost:3000/api/auth', + fetchOptions: { customFetchImpl: vi.fn() }, + }) + const safeClient = createVueSafeAuthProxy(client) + const store = reactive({ signIn: safeClient.signIn }) + + expect(isReactive(store.signIn)).toBe(false) + expect((store.signIn as Record).__v_skip).toBe(true) + }) + it('preserves call results and does not wrap returned promises', async () => { const fetch = vi.fn(async () => new Response('{}', { headers: { 'content-type': 'application/json' },