diff --git a/test/GoTrueClient.browser.test.ts b/test/GoTrueClient.browser.test.ts index 8107acc3..89248b81 100644 --- a/test/GoTrueClient.browser.test.ts +++ b/test/GoTrueClient.browser.test.ts @@ -4,10 +4,17 @@ import { autoRefreshClient, getClientWithSpecificStorage, pkceClient } from './lib/clients' import { mockUserCredentials } from './lib/utils' +import { + supportsLocalStorage, + validateExp, + sleep, + userNotAvailableProxy, + resolveFetch, +} from '../src/lib/helpers' // Add structuredClone polyfill for jsdom if (typeof structuredClone === 'undefined') { - ;(global as any).structuredClone = (obj: any) => JSON.parse(JSON.stringify(obj)) + ; (global as any).structuredClone = (obj: any) => JSON.parse(JSON.stringify(obj)) } describe('GoTrueClient in browser environment', () => { @@ -100,6 +107,49 @@ describe('GoTrueClient in browser environment', () => { }) }) +describe('Browser-specific helper functions', () => { + it('should handle localStorage not available', () => { + // Mock localStorage as undefined + Object.defineProperty(window, 'localStorage', { + value: undefined, + writable: true, + }) + expect(supportsLocalStorage()).toBe(false) + }) +}) + +describe('JWT and cryptographic functions in browser', () => { + it('should throw on missing exp claim', () => { + expect(() => validateExp(0)).toThrow('Missing exp claim') + }) +}) + +describe('Retryable and sleep functions in browser', () => { + it('should sleep for specified time', async () => { + const start = Date.now() + await sleep(100) + const end = Date.now() + expect(end - start).toBeGreaterThanOrEqual(90) + }) +}) + +describe('User proxy and deep clone functions in browser', () => { + it('should throw on property setting to user proxy', () => { + const proxy = userNotAvailableProxy() + expect(() => { + (proxy as any).email = 'test@example.com' + }).toThrow() + }) +}) + +describe('Fetch resolution in browser environment', () => { + it('should resolve fetch correctly', () => { + const customFetch = jest.fn() + const resolvedFetch = resolveFetch(customFetch) + expect(typeof resolvedFetch).toBe('function') + }) +}) + describe('Callback URL handling', () => { let mockFetch: jest.Mock let storedSession: string | null @@ -241,4 +291,88 @@ describe('GoTrueClient BroadcastChannel', () => { sub1.unsubscribe() sub2.unsubscribe() }) + + it('should handle BroadcastChannel errors', () => { + const mockBroadcastChannel = jest.fn().mockImplementation(() => { + throw new Error('BroadcastChannel not supported') + }) + + Object.defineProperty(window, 'BroadcastChannel', { + value: mockBroadcastChannel, + writable: true, + }) + + const client = getClientWithSpecificStorage({ + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + }) + + expect(client).toBeDefined() + }) +}) + +describe('Browser locks functionality', () => { + it('should use navigator locks when available', () => { + // Mock navigator.locks + const mockLock = { name: 'test-lock' } + const mockRequest = jest.fn().mockImplementation((_, __, callback) => + Promise.resolve(callback(mockLock)) + ) + + Object.defineProperty(navigator, 'locks', { + value: { request: mockRequest }, + writable: true, + }) + + // Test navigator locks usage in GoTrueClient + const client = getClientWithSpecificStorage({ + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + }) + + expect(client).toBeDefined() + }) +}) + +describe('Web3 functionality in browser', () => { + it('should handle Web3 provider not available', async () => { + const credentials = { + chain: 'ethereum' as const, + wallet: {} as any, + } + + await expect(pkceClient.signInWithWeb3(credentials)).rejects.toThrow() + }) + + it('should handle Solana Web3 provider not available', async () => { + const credentials = { + chain: 'solana' as const, + wallet: {} as any, + } + + await expect(pkceClient.signInWithWeb3(credentials)).rejects.toThrow() + }) +}) + +describe('GoTrueClient constructor edge cases', () => { + + it('should handle userStorage with persistSession', () => { + const customUserStorage = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + } + + const client = new (require('../src/GoTrueClient').default)({ + url: 'http://localhost:9999', + userStorage: customUserStorage, + persistSession: true, + autoRefreshToken: false, + }) + + expect(client).toBeDefined() + expect((client as any).userStorage).toBe(customUserStorage) + }) }) diff --git a/test/GoTrueClient.test.ts b/test/GoTrueClient.test.ts index 772b63e8..c5502ad6 100644 --- a/test/GoTrueClient.test.ts +++ b/test/GoTrueClient.test.ts @@ -492,15 +492,49 @@ describe('GoTrueClient', () => { expect(error?.message).toContain('Unable to validate email address: invalid format') }) - test('resend() with email', async () => { - const { email } = mockUserCredentials() - - const { error } = await auth.resend({ - email, - type: 'signup', - options: { emailRedirectTo: email }, - }) + test.each([ + { + name: 'resend with email with options', + params: { + email: mockUserCredentials().email, + type: 'signup' as const, + options: { emailRedirectTo: mockUserCredentials().email }, + }, + }, + { + name: 'resend with email with empty options', + params: { + email: mockUserCredentials().email, + type: 'signup' as const, + options: {}, + }, + }, + ])('$name', async ({ params }) => { + const { error } = await auth.resend(params) + expect(error).toBeNull() + }) + test.each([ + { + name: 'resend with phone with options', + params: { + phone: mockUserCredentials().phone, + type: 'phone_change' as const, + options: { + captchaToken: 'some_token', + }, + }, + }, + { + name: 'resend with phone with empty options', + params: { + phone: mockUserCredentials().phone, + type: 'phone_change' as const, + options: {}, + }, + }, + ])('$name', async ({ params }) => { + const { error } = await phoneClient.resend(params) expect(error).toBeNull() }) @@ -540,6 +574,40 @@ describe('GoTrueClient', () => { expect(data.session).toBeNull() }) + test.each([ + { + name: 'signInWithOtp for email with empty options', + testFn: async () => { + const { email } = mockUserCredentials() + const { data, error } = await auth.signInWithOtp({ + email, + options: {}, + }) + expect(error).toBeNull() + expect(data.user).toBeNull() + expect(data.session).toBeNull() + }, + }, + { + name: 'verifyOTP fails with empty options', + testFn: async () => { + const { phone } = mockUserCredentials() + + const { error } = await phoneClient.verifyOtp({ + phone, + type: 'phone_change', + token: '123456', + options: {}, + }) + + expect(error).not.toBeNull() + expect(error?.message).toContain('Token has expired or is invalid') + }, + }, + ])('$name', async ({ testFn }) => { + await testFn() + }) + test('signInWithOtp() pkce flow fails with invalid sms provider', async () => { const { phone } = mockUserCredentials() @@ -634,19 +702,7 @@ describe('GoTrueClient', () => { } }) - test('resend() with phone', async () => { - const { phone } = mockUserCredentials() - - const { error } = await phoneClient.resend({ - phone, - type: 'phone_change', - options: { - captchaToken: 'some_token', - }, - }) - expect(error).toBeNull() - }) test('verifyOTP() fails with invalid token', async () => { const { phone } = mockUserCredentials() @@ -664,6 +720,8 @@ describe('GoTrueClient', () => { expect(error).not.toBeNull() expect(error?.message).toContain('Token has expired or is invalid') }) + + }) test('signUp() the same user twice should not share email already registered message', async () => { @@ -1266,13 +1324,13 @@ describe('MFA', () => { test('challenge should create MFA challenge', async () => { const { factorId } = await setupUserWithMFAAndTOTP() - const { data: challengeData, error: challengeError } = await authWithSession.mfa.challenge({ + const { data, error: challengeError } = await authWithSession.mfa.challenge({ factorId, }) expect(challengeError).toBeNull() - expect(challengeData!.id).not.toBeNull() - expect(challengeData!.expires_at).not.toBeNull() + expect(data!.id).not.toBeNull() + expect(data!.expires_at).not.toBeNull() }) test('verify should verify MFA challenge', async () => { @@ -2324,8 +2382,8 @@ describe('Storage adapter edge cases', () => { getItem: async () => { throw new Error('getItem failed message') }, - setItem: async () => {}, - removeItem: async () => {}, + setItem: async () => { }, + removeItem: async () => { }, } const client = getClientWithSpecificStorage(brokenStorage) await expect(client.getSession()).rejects.toThrow('getItem failed message') @@ -2337,7 +2395,7 @@ describe('Storage adapter edge cases', () => { setItem: async () => { throw new Error('setItem failed message') }, - removeItem: async () => {}, + removeItem: async () => { }, } const client = getClientWithSpecificStorage(brokenStorage) const session = { @@ -2358,7 +2416,7 @@ describe('Storage adapter edge cases', () => { test('should handle storage removeItem failure in _removeSession', async () => { const brokenStorage = { getItem: async () => '{}', - setItem: async () => {}, + setItem: async () => { }, removeItem: async () => { throw new Error('removeItem failed message') }, @@ -2371,8 +2429,8 @@ describe('Storage adapter edge cases', () => { test('should handle invalid JSON in storage', async () => { const invalidStorage = { getItem: async () => 'invalid-json', - setItem: async () => {}, - removeItem: async () => {}, + setItem: async () => { }, + removeItem: async () => { }, } const client = getClientWithSpecificStorage(invalidStorage) const { data, error } = await client.getSession() @@ -2383,8 +2441,8 @@ describe('Storage adapter edge cases', () => { test('should handle null storage value', async () => { const nullStorage = { getItem: async () => null, - setItem: async () => {}, - removeItem: async () => {}, + setItem: async () => { }, + removeItem: async () => { }, } const client = getClientWithSpecificStorage(nullStorage) const { data, error } = await client.getSession() @@ -2395,8 +2453,8 @@ describe('Storage adapter edge cases', () => { test('should handle empty storage value', async () => { const emptyStorage = { getItem: async () => '', - setItem: async () => {}, - removeItem: async () => {}, + setItem: async () => { }, + removeItem: async () => { }, } const client = getClientWithSpecificStorage(emptyStorage) const { data, error } = await client.getSession() @@ -2407,8 +2465,8 @@ describe('Storage adapter edge cases', () => { test('should handle malformed session data', async () => { const malformedStorage = { getItem: async () => JSON.stringify({ access_token: 'test' }), // Missing required fields - setItem: async () => {}, - removeItem: async () => {}, + setItem: async () => { }, + removeItem: async () => { }, } const client = getClientWithSpecificStorage(malformedStorage) const { data, error } = await client.getSession() @@ -2427,8 +2485,8 @@ describe('Storage adapter edge cases', () => { token_type: 'bearer', user: null, }), - setItem: async () => {}, - removeItem: async () => {}, + setItem: async () => { }, + removeItem: async () => { }, } const client = getClientWithSpecificStorage(expiredStorage) // @ts-expect-error private method @@ -2488,6 +2546,7 @@ describe('SSO Authentication', () => { providerId: 'valid-provider-id', options: { redirectTo: 'http://localhost:3000/callback', + captchaToken: 'some-token', }, }) @@ -2495,6 +2554,34 @@ describe('SSO Authentication', () => { expect(error?.message).toContain('SAML 2.0 is disabled') expect(data).toBeNull() }) + + test.each([ + { + name: 'with empty options', + params: { + providerId: 'valid-provider-id', + domain: 'valid-domain', + options: {}, + }, + }, + { + name: 'without params', + params: {} as any, + }, + { + name: 'with minimal params', + params: { + providerId: 'test-provider', + domain: 'test-domain', + }, + }, + ])('signInWithSSO $name', async ({ params }) => { + const { data, error } = await pkceClient.signInWithSSO(params) + + expect(error).not.toBeNull() + expect(error?.message).toContain('SAML 2.0 is disabled') + expect(data).toBeNull() + }) }) describe('Lock functionality', () => {