From 6617c8c509e8df61fc228583b35e6f7b0cf8bb08 Mon Sep 17 00:00:00 2001 From: rusanov Date: Thu, 31 Jul 2025 23:59:23 +0200 Subject: [PATCH 1/5] Added more browser tests --- test/GoTrueClient.browser.test.ts | 115 +++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/test/GoTrueClient.browser.test.ts b/test/GoTrueClient.browser.test.ts index 8107acc3..9820d132 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,67 @@ 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() + }) }) From 7835df57eaade75dbdc30267294d76371fd81897 Mon Sep 17 00:00:00 2001 From: rusanov Date: Fri, 1 Aug 2025 01:05:49 +0200 Subject: [PATCH 2/5] added GoTrueClient tests --- test/GoTrueClient.browser.test.ts | 21 ++++ test/GoTrueClient.test.ts | 161 +++++++++++++++++++++++------- 2 files changed, 145 insertions(+), 37 deletions(-) diff --git a/test/GoTrueClient.browser.test.ts b/test/GoTrueClient.browser.test.ts index 9820d132..89248b81 100644 --- a/test/GoTrueClient.browser.test.ts +++ b/test/GoTrueClient.browser.test.ts @@ -355,3 +355,24 @@ describe('Web3 functionality in browser', () => { 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', () => { From ae1bab2c5c14198dbb5f294c7b5bff72fe2d619f Mon Sep 17 00:00:00 2001 From: rusanov Date: Thu, 14 Aug 2025 17:38:25 +0200 Subject: [PATCH 3/5] chore: added tests for ethereum.ts --- test/ethereum.test.ts | 314 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 test/ethereum.test.ts diff --git a/test/ethereum.test.ts b/test/ethereum.test.ts new file mode 100644 index 00000000..13121e2b --- /dev/null +++ b/test/ethereum.test.ts @@ -0,0 +1,314 @@ +import { + getAddress, + fromHex, + toHex, + createSiweMessage, + type SiweMessage, + type Hex, +} from '../src/lib/web3/ethereum' + +describe('ethereum', () => { + describe('getAddress', () => { + test('should return lowercase address for valid Ethereum address', () => { + const validAddresses = [ + '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6', + '0x742d35cc6634c0532925a3b8d4c9db96c4b4d8b6', + '0x1234567890123456789012345678901234567890', + '0xABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCD', + ] + + validAddresses.forEach((address) => { + const result = getAddress(address) + expect(result).toBe(address.toLowerCase()) + expect(result).toMatch(/^0x[a-f0-9]{40}$/) + }) + }) + + test('should throw error for invalid address format', () => { + const invalidAddresses = [ + '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b', // too short + '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b67', // too long + '742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6', // missing 0x + '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8bG', // invalid character + '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b!', // invalid character + '', // empty string + 'not-an-address', // random string + ] + + invalidAddresses.forEach((address) => { + expect(() => getAddress(address)).toThrow( + `@supabase/auth-js: Address "${address}" is invalid.` + ) + }) + }) + + test('should handle edge cases', () => { + // Valid address with all zeros + expect(getAddress('0x0000000000000000000000000000000000000000')).toBe( + '0x0000000000000000000000000000000000000000' + ) + + // Valid address with all f's + expect(getAddress('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF')).toBe( + '0xffffffffffffffffffffffffffffffffffffffff' + ) + }) + }) + + describe('fromHex', () => { + test('should convert hex to number', () => { + const testCases: Array<{ hex: Hex; expected: number }> = [ + { hex: '0x0', expected: 0 }, + { hex: '0x1', expected: 1 }, + { hex: '0xff', expected: 255 }, + { hex: '0x100', expected: 256 }, + { hex: '0xffff', expected: 65535 }, + { hex: '0x123456', expected: 1193046 }, + { hex: '0x7fffffff', expected: 2147483647 }, + ] + + testCases.forEach(({ hex, expected }) => { + expect(fromHex(hex)).toBe(expected) + }) + }) + + test('should handle uppercase and lowercase hex', () => { + expect(fromHex('0xFF')).toBe(255) + expect(fromHex('0xff')).toBe(255) + expect(fromHex('0xFf')).toBe(255) + }) + }) + + describe('toHex', () => { + test('should convert string to hex', () => { + const testCases: Array<{ input: string; expected: Hex }> = [ + { input: '', expected: '0x' }, + { input: 'a', expected: '0x61' }, + { input: 'hello', expected: '0x68656c6c6f' }, + { input: 'Hello World!', expected: '0x48656c6c6f20576f726c6421' }, + { input: '123', expected: '0x313233' }, + { input: 'привет', expected: '0xd0bfd180d0b8d0b2d0b5d182' }, + ] + + testCases.forEach(({ input, expected }) => { + expect(toHex(input)).toBe(expected) + }) + }) + + test('should handle special characters', () => { + expect(toHex('\n')).toBe('0x0a') + expect(toHex('\t')).toBe('0x09') + expect(toHex('\r')).toBe('0x0d') + expect(toHex(' ')).toBe('0x20') + expect(toHex('!@#$%^&*()')).toBe('0x21402324255e262a2829') + }) + }) + + describe('createSiweMessage', () => { + const baseMessage: SiweMessage = { + address: '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6', + chainId: 1, + domain: 'example.com', + uri: 'https://example.com', + version: '1', + } + + test('should create basic SIWE message', () => { + const message = createSiweMessage(baseMessage) + + expect(message).toContain('example.com wants you to sign in with your Ethereum account:') + expect(message).toContain('0x742d35cc6634c0532925a3b8d4c9db96c4b4d8b6') + expect(message).toContain('URI: https://example.com') + expect(message).toContain('Version: 1') + expect(message).toContain('Chain ID: 1') + expect(message).toContain('Issued At:') + }) + + test('should include optional fields when provided', () => { + const messageWithOptions: SiweMessage = { + ...baseMessage, + statement: 'Please sign this message to authenticate', + nonce: '1234567890', + expirationTime: new Date('2024-12-31T23:59:59Z'), + notBefore: new Date('2024-01-01T00:00:00Z'), + requestId: 'req-123', + resources: ['https://example.com/resource1', 'https://example.com/resource2'], + scheme: 'https', + } + + const message = createSiweMessage(messageWithOptions) + + expect(message).toContain('Please sign this message to authenticate') + expect(message).toContain('Nonce: 1234567890') + expect(message).toContain('Expiration Time: 2024-12-31T23:59:59.000Z') + expect(message).toContain('Not Before: 2024-01-01T00:00:00.000Z') + expect(message).toContain('Request ID: req-123') + expect(message).toContain('Resources:') + expect(message).toContain('- https://example.com/resource1') + expect(message).toContain('- https://example.com/resource2') + expect(message).toContain('https://example.com wants you to sign in') + }) + + test('should handle scheme correctly', () => { + const messageWithScheme: SiweMessage = { + ...baseMessage, + scheme: 'https', + } + + const message = createSiweMessage(messageWithScheme) + expect(message).toContain('https://example.com wants you to sign in') + }) + + test('should validate chainId', () => { + const invalidChainId: SiweMessage = { + ...baseMessage, + chainId: 1.5, // non-integer + } + + expect(() => createSiweMessage(invalidChainId)).toThrow( + '@supabase/auth-js: Invalid SIWE message field "chainId". Chain ID must be a EIP-155 chain ID. Provided value: 1.5' + ) + }) + + test('should validate domain', () => { + const invalidDomain: SiweMessage = { + ...baseMessage, + domain: '', // empty domain + } + + expect(() => createSiweMessage(invalidDomain)).toThrow( + '@supabase/auth-js: Invalid SIWE message field "domain". Domain must be provided.' + ) + }) + + test('should validate nonce length', () => { + const shortNonce: SiweMessage = { + ...baseMessage, + nonce: '123', // too short + } + + expect(() => createSiweMessage(shortNonce)).toThrow( + '@supabase/auth-js: Invalid SIWE message field "nonce". Nonce must be at least 8 characters. Provided value: 123' + ) + }) + + test('should validate uri', () => { + const invalidUri: SiweMessage = { + ...baseMessage, + uri: '', // empty uri + } + + expect(() => createSiweMessage(invalidUri)).toThrow( + '@supabase/auth-js: Invalid SIWE message field "uri". URI must be provided.' + ) + }) + + test('should validate version', () => { + const invalidVersion: SiweMessage = { + ...baseMessage, + version: '2' as any, // invalid version + } + + expect(() => createSiweMessage(invalidVersion)).toThrow( + '@supabase/auth-js: Invalid SIWE message field "version". Version must be \'1\'. Provided value: 2' + ) + }) + + test('should validate statement does not contain newlines', () => { + const invalidStatement: SiweMessage = { + ...baseMessage, + statement: 'Line 1\nLine 2', // contains newline + } + + expect(() => createSiweMessage(invalidStatement)).toThrow( + '@supabase/auth-js: Invalid SIWE message field "statement". Statement must not include \'\\n\'. Provided value: Line 1\nLine 2' + ) + }) + + test('should validate resources array', () => { + const invalidResources: SiweMessage = { + ...baseMessage, + resources: ['valid-resource', '', 'another-valid'], // contains empty string + } + + expect(() => createSiweMessage(invalidResources)).toThrow( + '@supabase/auth-js: Invalid SIWE message field "resources". Every resource must be a valid string. Provided value: ' + ) + }) + + test('should validate resources are strings', () => { + const invalidResources: SiweMessage = { + ...baseMessage, + resources: ['valid-resource', null as any, 'another-valid'], // contains null + } + + expect(() => createSiweMessage(invalidResources)).toThrow( + '@supabase/auth-js: Invalid SIWE message field "resources". Every resource must be a valid string. Provided value: null' + ) + }) + + test('should handle empty resources array', () => { + const messageWithEmptyResources: SiweMessage = { + ...baseMessage, + resources: [], + } + + const message = createSiweMessage(messageWithEmptyResources) + expect(message).toContain('Resources:') + }) + + test('should format message correctly with all optional fields', () => { + const fullMessage: SiweMessage = { + address: '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6', + chainId: 137, + domain: 'polygon.example.com', + uri: 'https://polygon.example.com/auth', + version: '1', + statement: 'Sign in to access your account', + nonce: 'abcdef1234567890', + expirationTime: new Date('2024-12-31T23:59:59Z'), + notBefore: new Date('2024-01-01T00:00:00Z'), + requestId: 'auth-request-12345', + resources: [ + 'https://polygon.example.com/api', + 'https://polygon.example.com/dashboard', + ], + scheme: 'https', + } + + const message = createSiweMessage(fullMessage) + + // Check the structure + const lines = message.split('\n') + expect(lines[0]).toBe('https://polygon.example.com wants you to sign in with your Ethereum account:') + expect(lines[1]).toBe('0x742d35cc6634c0532925a3b8d4c9db96c4b4d8b6') + expect(lines[2]).toBe('') + expect(lines[3]).toBe('Sign in to access your account') + expect(lines[4]).toBe('') + expect(lines[5]).toBe('URI: https://polygon.example.com/auth') + expect(lines[6]).toBe('Version: 1') + expect(lines[7]).toBe('Chain ID: 137') + expect(lines[8]).toBe('Nonce: abcdef1234567890') + expect(lines[9]).toMatch(/^Issued At: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + expect(lines[10]).toBe('Expiration Time: 2024-12-31T23:59:59.000Z') + expect(lines[11]).toBe('Not Before: 2024-01-01T00:00:00.000Z') + expect(lines[12]).toBe('Request ID: auth-request-12345') + expect(lines[13]).toBe('Resources:') + expect(lines[14]).toBe('- https://polygon.example.com/api') + expect(lines[15]).toBe('- https://polygon.example.com/dashboard') + }) + + test('should handle issuedAt default value', () => { + const beforeTest = new Date() + const message = createSiweMessage(baseMessage) + const afterTest = new Date() + + const issuedAtMatch = message.match(/Issued At: (.+)/) + expect(issuedAtMatch).toBeTruthy() + + const issuedAt = new Date(issuedAtMatch![1]) + expect(issuedAt.getTime()).toBeGreaterThanOrEqual(beforeTest.getTime()) + expect(issuedAt.getTime()).toBeLessThanOrEqual(afterTest.getTime()) + }) + }) +}) From e3211e763857be589fdb7044382ee3f286dec06d Mon Sep 17 00:00:00 2001 From: rusanov Date: Thu, 14 Aug 2025 19:00:51 +0200 Subject: [PATCH 4/5] chore: Added more goTrueClient tests --- test/GoTrueClient.test.ts | 313 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) diff --git a/test/GoTrueClient.test.ts b/test/GoTrueClient.test.ts index c5502ad6..8e2f4cde 100644 --- a/test/GoTrueClient.test.ts +++ b/test/GoTrueClient.test.ts @@ -39,6 +39,39 @@ describe('GoTrueClient', () => { refreshAccessTokenSpy.mockClear() }) + test('should handle debug function in settings', async () => { + const debugSpy = jest.fn() + const client = new GoTrueClient({ + url: 'http://localhost:9999', + debug: debugSpy, + }) + + await client.initialize() + expect(debugSpy).toHaveBeenCalled() + }) + + test('should handle custom lock implementation', async () => { + const customLock = jest.fn().mockResolvedValue(undefined) + const client = new GoTrueClient({ + url: 'http://localhost:9999', + lock: customLock, + }) + + await client.initialize() + expect(customLock).toHaveBeenCalled() + }) + + test('should handle userStorage configuration', async () => { + const userStorage = memoryLocalStorageAdapter() + const client = new GoTrueClient({ + url: 'http://localhost:9999', + userStorage, + }) + + await client.initialize() + expect(client['userStorage']).toBe(userStorage) + }) + describe('Sessions', () => { test('refreshSession() should return a new session using a passed-in refresh token', async () => { const { email, password } = mockUserCredentials() @@ -557,6 +590,67 @@ describe('GoTrueClient', () => { expect(data.session).toBeNull() expect(data.user).toBeNull() }) + + test('should handle signUp with invalid credentials', async () => { + const { data, error } = await auth.signUp({ + email: 'invalid-email', + password: '123', // too short + }) + + expect(error).toBeDefined() + expect(data.user).toBeNull() + expect(data.session).toBeNull() + }) + + test('should handle signInWithPassword with invalid credentials', async () => { + const { data, error } = await auth.signInWithPassword({ + email: 'nonexistent@example.com', + password: 'wrongpassword', + }) + + expect(error).toBeDefined() + expect(data.user).toBeNull() + expect(data.session).toBeNull() + }) + + test('should handle signInWithOAuth with invalid provider', async () => { + const { data, error } = await auth.signInWithOAuth({ + provider: 'invalid-provider' as any, + }) + + expect(error).toBeNull() + expect(data.url).toBeDefined() + }) + + test('should handle signInWithOtp with invalid email', async () => { + const { data, error } = await auth.signInWithOtp({ + email: 'invalid-email', + }) + + expect(error).toBeDefined() + expect(data.user).toBeNull() + }) + + test('should handle signInWithOtp with invalid phone', async () => { + const { data, error } = await auth.signInWithOtp({ + phone: 'invalid-phone', + }) + + expect(error).toBeDefined() + expect(data.user).toBeNull() + }) + + test('should handle verifyOtp with invalid code', async () => { + const { data, error } = await auth.verifyOtp({ + email: 'test@example.com', + token: 'invalid-token', + type: 'email', + }) + + expect(error).toBeDefined() + expect(data.user).toBeNull() + expect(data.session).toBeNull() + }) }) describe('signInWithOtp', () => { @@ -1146,6 +1240,40 @@ describe('The auth client can signin with third-party oAuth providers', () => { expect(provider).toBeTruthy() }) + test('should handle exchangeCodeForSession with invalid code', async () => { + const { data, error } = await auth.exchangeCodeForSession('invalid-code') + expect(error).toBeDefined() + expect(data.session).toBeNull() + expect(data.user).toBeNull() + }) + + test('should handle signInWithOAuth with redirectTo option', async () => { + const { data, error } = await auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: 'http://localhost:3000/callback', + }, + }) + + expect(error).toBeNull() + expect(data.url).toBeDefined() + }) + + test('should handle signInWithOAuth with queryParams', async () => { + const { data, error } = await auth.signInWithOAuth({ + provider: 'github', + options: { + queryParams: { + access_type: 'offline', + prompt: 'consent', + }, + }, + }) + + expect(error).toBeNull() + expect(data.url).toBeDefined() + }) + describe('Developers can subscribe and unsubscribe', () => { const { data: { subscription }, @@ -1422,6 +1550,45 @@ describe('MFA', () => { expect(result.data.phone).toHaveLength(1) } }) + + test('should handle MFA enroll without session', async () => { + await auth.signOut() + const { data, error } = await auth.mfa.enroll({ + factorType: 'totp', + }) + + expect(error).toBeDefined() + expect(data?.id).toBeUndefined() + }) + + test('should handle MFA challenge without session', async () => { + const { data, error } = await auth.mfa.challenge({ + factorId: 'test-factor-id', + }) + + expect(error).toBeDefined() + expect(data?.id).toBeUndefined() + }) + + test('should handle MFA verify without session', async () => { + const { data, error } = await auth.mfa.verify({ + factorId: 'test-factor-id', + challengeId: 'test-challenge-id', + code: '123456', + }) + + expect(error).toBeDefined() + expect(data?.access_token).toBeUndefined() + }) + + test('should handle MFA unenroll without session', async () => { + const { data, error } = await auth.mfa.unenroll({ + factorId: 'test-factor-id', + }) + + expect(error).toBeDefined() + expect(data?.id).toBeUndefined() + }) }) describe('getClaims', () => { @@ -2038,6 +2205,74 @@ describe('Web3 Authentication', () => { '@supabase/auth-js: No accounts available. Please ensure the wallet is connected.' ) }) + + test('should handle signInWithWeb3 with unsupported chain', async () => { + const credentials = { + chain: 'polygon' as any, + message: 'test message', + signature: '0xtest-signature' as any, + } + + await expect( + auth.signInWithWeb3(credentials) + ).rejects.toThrow('Unsupported chain "polygon"') + }) + + test('should handle signInWithWeb3 with missing message', async () => { + const credentials = { + chain: 'ethereum', + signature: 'test signature', + } as any + + await expect( + auth.signInWithWeb3(credentials) + ).rejects.toThrow('Both wallet and url must be specified in non-browser environments') + }) + + test('should handle signInWithWeb3 with missing wallet', async () => { + const credentials = { + chain: 'ethereum', + message: 'test message', + } as any + + const { data, error } = await auth.signInWithWeb3(credentials) + expect(error).toBeDefined() + expect(data.session).toBeNull() + expect(data.user).toBeNull() + }) + + test('should handle signInWithWeb3 with invalid signature', async () => { + const credentials = { + chain: 'ethereum', + message: 'test message', + signature: '0xinvalid-signature' as any, + } as any + + const { data, error } = await auth.signInWithWeb3(credentials) + expect(error).toBeDefined() + expect(data.session).toBeNull() + expect(data.user).toBeNull() + }) + + test('should handle signInWithWeb3 in non-browser environment', async () => { + const credentials = { + chain: 'ethereum', + message: 'test message', + wallet: { + request: jest.fn(), + on: jest.fn(), + removeListener: jest.fn(), + }, + options: { + url: 'http://localhost:9999', + }, + } as any + + const { data, error } = await auth.signInWithWeb3(credentials) + expect(error).toBeDefined() + expect(data.session).toBeNull() + expect(data.user).toBeNull() + }) }) describe('ID Token Authentication', () => { @@ -2239,6 +2474,14 @@ describe('Auto Refresh', () => { expect(session?.access_token).not.toEqual(signUpData.session?.access_token) }) }) + + test('should handle auto refresh start/stop multiple times', async () => { + await autoRefreshClient.startAutoRefresh() + await autoRefreshClient.startAutoRefresh() // Should handle duplicate calls + + await autoRefreshClient.stopAutoRefresh() + await autoRefreshClient.stopAutoRefresh() // Should handle duplicate calls + }) }) describe('Session Management', () => { @@ -2285,6 +2528,60 @@ describe('Session Management', () => { // Cleanup subscription?.unsubscribe() }) + + test('should handle getSession when no session exists', async () => { + // Clear any existing session first + await auth.signOut() + const { data, error } = await auth.getSession() + expect(data.session).toBeNull() + expect(error).toBeNull() + }) + + test('should handle refreshSession with invalid refresh token', async () => { + const { data, error } = await auth.refreshSession({ + refresh_token: 'invalid-refresh-token', + }) + + expect(error).toBeDefined() + expect(data.session).toBeNull() + }) + + test('should handle setSession with invalid session data', async () => { + const { data, error } = await auth.setSession({ + access_token: 'invalid-token', + refresh_token: 'invalid-refresh-token', + }) + + expect(error).toBeDefined() + expect(data.session).toBeNull() + }) + + test('should handle getUser without valid session', async () => { + await auth.signOut() + const { data, error } = await auth.getUser() + expect(data.user).toBeNull() + expect(error).toBeDefined() + }) + + test('should handle updateUser without valid session', async () => { + await auth.signOut() + const { data, error } = await auth.updateUser({ + data: { name: 'Test User' }, + }) + + expect(error).toBeDefined() + expect(data.user).toBeNull() + }) + + test('should handle custom URL parameters', async () => { + const client = new GoTrueClient({ + url: 'http://localhost:9999', + detectSessionInUrl: false, + }) + + await client.initialize() + expect(client['detectSessionInUrl']).toBe(false) + }) }) describe('Session Management Edge Cases', () => { @@ -2538,6 +2835,22 @@ describe('Storage adapter edge cases', () => { expect(url).toContain('code_challenge=') expect(url).toContain('code_challenge_method=') }) + + test('should handle localStorage not supported', async () => { + // Mock supportsLocalStorage to return false + jest.doMock('../src/lib/helpers', () => ({ + ...jest.requireActual('../src/lib/helpers'), + supportsLocalStorage: () => false, + })) + + const client = new GoTrueClient({ + url: 'http://localhost:9999', + }) + + await client.initialize() + // Should fall back to memory storage + expect(client['storage']).toBeDefined() + }) }) describe('SSO Authentication', () => { From d8b8a4e87f458736c115cca8bb0f93c30fd0eb36 Mon Sep 17 00:00:00 2001 From: rusanov Date: Thu, 14 Aug 2025 21:25:07 +0200 Subject: [PATCH 5/5] chore: more tests --- test/GoTrueClient.browser.test.ts | 341 ++++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) diff --git a/test/GoTrueClient.browser.test.ts b/test/GoTrueClient.browser.test.ts index 89248b81..a3fe50d2 100644 --- a/test/GoTrueClient.browser.test.ts +++ b/test/GoTrueClient.browser.test.ts @@ -105,6 +105,31 @@ describe('GoTrueClient in browser environment', () => { expect(signinError).toBeNull() expect(signinData?.session).toBeDefined() }) + + it('should handle _handleVisibilityChange error handling', async () => { + const client = getClientWithSpecificStorage({ + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + }) + + // Mock window.addEventListener to throw error + const originalAddEventListener = window.addEventListener + window.addEventListener = jest.fn().mockImplementation(() => { + throw new Error('addEventListener failed') + }) + + try { + // Initialize client to trigger _handleVisibilityChange + await client.initialize() + + // Should not throw error, should handle it gracefully + expect(client).toBeDefined() + } finally { + // Restore original addEventListener + window.addEventListener = originalAddEventListener + } + }) }) describe('Browser-specific helper functions', () => { @@ -234,6 +259,65 @@ describe('Callback URL handling', () => { } = await client.getSession() expect(session).toBeNull() }) + + it('should handle _initialize with detectSessionInUrl', async () => { + // Mock window.location with session parameters + Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost:9999/callback?access_token=test&refresh_token=test&expires_in=3600&token_type=bearer&type=recovery', + assign: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + toString: () => 'http://localhost:9999/callback', + }, + writable: true, + }) + + const client = new (require('../src/GoTrueClient').default)({ + url: 'http://localhost:9999', + detectSessionInUrl: true, + autoRefreshToken: false, + }) + + // Initialize client to trigger _initialize with detectSessionInUrl + await client.initialize() + + expect(client).toBeDefined() + }) + + it('should handle _initialize with PKCE flow mismatch', async () => { + // Mock window.location with PKCE parameters + Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost:9999/callback?code=test-code', + assign: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + toString: () => 'http://localhost:9999/callback', + }, + writable: true, + }) + + // Mock storage to return code verifier + const mockStorage = { + getItem: jest.fn().mockResolvedValue('test-code-verifier'), + setItem: jest.fn(), + removeItem: jest.fn(), + } + + const client = new (require('../src/GoTrueClient').default)({ + url: 'http://localhost:9999', + detectSessionInUrl: true, + autoRefreshToken: false, + storage: mockStorage, + flowType: 'implicit', // Mismatch with PKCE flow + }) + + // Initialize client to trigger flow mismatch + await client.initialize() + + expect(client).toBeDefined() + }) }) describe('GoTrueClient BroadcastChannel', () => { @@ -334,6 +418,30 @@ describe('Browser locks functionality', () => { expect(client).toBeDefined() }) + + it('should handle _acquireLock with empty pendingInLock', async () => { + const client = getClientWithSpecificStorage({ + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + }) + + // 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, + }) + + // Initialize client to trigger lock acquisition + await client.initialize() + + expect(client).toBeDefined() + }) }) describe('Web3 functionality in browser', () => { @@ -376,3 +484,236 @@ describe('GoTrueClient constructor edge cases', () => { expect((client as any).userStorage).toBe(customUserStorage) }) }) + +describe('linkIdentity with skipBrowserRedirect false', () => { + + it('should linkIdentity with skipBrowserRedirect false', async () => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost:9999', + assign: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + toString: () => 'http://localhost:9999', + }, + writable: true, + }) + // Mock successful session + const mockSession = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600, + token_type: 'bearer', + user: { id: 'test-user' }, + } + + // Mock storage to return session + const mockStorage = { + getItem: jest.fn().mockResolvedValue(JSON.stringify(mockSession)), + setItem: jest.fn(), + removeItem: jest.fn(), + } + + // Create client with custom fetch + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ url: 'http://localhost:9999/oauth/callback' }), + text: () => Promise.resolve('{"url": "http://localhost:9999/oauth/callback"}'), + headers: new Headers(), + } as Response) + + const clientWithSession = new (require('../src/GoTrueClient').default)({ + url: 'http://localhost:9999', + storageKey: 'test-specific-storage', + autoRefreshToken: false, + persistSession: true, + storage: mockStorage, + fetch: mockFetch, + }) + + // Mock window.location.assign + const mockAssign = jest.fn() + Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost:9999', + assign: mockAssign, + replace: jest.fn(), + reload: jest.fn(), + toString: () => 'http://localhost:9999', + }, + writable: true, + }) + + try { + const result = await clientWithSession.linkIdentity({ + provider: 'github', + options: { + skipBrowserRedirect: false, + }, + }) + + expect(result.data?.url).toBeDefined() + expect(mockFetch).toHaveBeenCalled() + // Note: linkIdentity might not always call window.location.assign depending on the response + // So we just verify the result is defined + } catch (error) { + console.error('Test error:', error) + throw error + } + }) +}) + +describe('Session Management Tests', () => { + it('should handle _recoverAndRefresh with Infinity expires_at', async () => { + // Mock session with null expires_at + const mockSession = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600, + expires_at: null, + token_type: 'bearer', + user: { id: 'test-user' }, + } + + const mockStorage = { + getItem: jest.fn().mockResolvedValue(JSON.stringify(mockSession)), + setItem: jest.fn(), + removeItem: jest.fn(), + } + + const client = getClientWithSpecificStorage(mockStorage) + + // Initialize client to trigger _recoverAndRefresh with Infinity expires_at + await client.initialize() + + expect(client).toBeDefined() + }) + + it('should handle _recoverAndRefresh with refresh token error', async () => { + // Mock session + const mockSession = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600, + expires_at: Math.floor(Date.now() / 1000) - 100, // Expired + token_type: 'bearer', + user: { id: 'test-user' }, + } + + const mockStorage = { + getItem: jest.fn().mockResolvedValue(JSON.stringify(mockSession)), + setItem: jest.fn(), + removeItem: jest.fn(), + } + + const mockFetch = jest.fn().mockResolvedValue({ + ok: false, + status: 401, + json: () => Promise.resolve({ error: 'invalid_grant' }), + text: () => Promise.resolve('{"error": "invalid_grant"}'), + headers: new Headers(), + } as Response) + + const client = new (require('../src/GoTrueClient').default)({ + url: 'http://localhost:9999', + autoRefreshToken: true, + persistSession: true, + storage: mockStorage, + fetch: mockFetch, + }) + + // Initialize client to trigger refresh token error + await client.initialize() + + expect(client).toBeDefined() + }) + + it('should handle _recoverAndRefresh with user proxy', async () => { + // Mock session with proxy user + const mockSession = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expires_in: 3600, + expires_at: Math.floor(Date.now() / 1000) + 3600, // Valid + token_type: 'bearer', + user: { __isUserNotAvailableProxy: true }, + } + + const mockStorage = { + getItem: jest.fn().mockResolvedValue(JSON.stringify(mockSession)), + setItem: jest.fn(), + removeItem: jest.fn(), + } + + // Mock fetch to return user data + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ user: { id: 'test-user', email: 'test@example.com' } }), + text: () => Promise.resolve('{"user": {"id": "test-user", "email": "test@example.com"}}'), + headers: new Headers(), + } as Response) + + const client = new (require('../src/GoTrueClient').default)({ + url: 'http://localhost:9999', + autoRefreshToken: true, + persistSession: true, + storage: mockStorage, + fetch: mockFetch, + }) + + // Initialize client to trigger user proxy handling + await client.initialize() + + expect(client).toBeDefined() + }) +}) + +describe('Additional Tests', () => { + it('should handle _initialize with storage returning boolean', async () => { + // Mock storage to return boolean + const mockStorage = { + getItem: jest.fn().mockResolvedValue(true), + setItem: jest.fn(), + removeItem: jest.fn(), + } + + const client = new (require('../src/GoTrueClient').default)({ + url: 'http://localhost:9999', + autoRefreshToken: false, + persistSession: true, + storage: mockStorage, + }) + + // Initialize client to trigger boolean handling + await client.initialize() + + expect(client).toBeDefined() + }) + + it('should handle _initialize with expires_at parameter', async () => { + // Mock window.location with expires_at parameter + Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost:9999/callback?access_token=test&refresh_token=test&expires_in=3600&expires_at=1234567890&token_type=bearer', + assign: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + toString: () => 'http://localhost:9999/callback', + }, + writable: true, + }) + + const client = new (require('../src/GoTrueClient').default)({ + url: 'http://localhost:9999', + detectSessionInUrl: true, + autoRefreshToken: false, + }) + + // Initialize client to trigger _initialize with expires_at + await client.initialize() + + expect(client).toBeDefined() + }) +})