diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5105729a..da40a7239 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -316,6 +316,14 @@ jobs: - name: Start Supabase run: supabase start + - name: Create storage bucket for tests + run: | + SERVICE_ROLE_KEY="$(supabase status --output json | jq -r '.SERVICE_ROLE_KEY')" + curl -X POST 'http://127.0.0.1:54321/storage/v1/bucket' \ + -H "Authorization: Bearer $SERVICE_ROLE_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"id": "test-bucket", "name": "test-bucket", "public": true}' + - name: Install dependencies and run tests run: | cd test/integration/bun diff --git a/package-lock.json b/package-lock.json index f82d2e6b1..12d09e119 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1589,19 +1589,6 @@ "dev": true, "license": "MIT" }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", @@ -2564,6 +2551,7 @@ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -2572,28 +2560,40 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, + "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/compression/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -2602,7 +2602,29 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", @@ -5699,10 +5721,11 @@ "dev": true }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -5997,10 +6020,11 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -7168,9 +7192,9 @@ } }, "node_modules/serve": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz", - "integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.5.tgz", + "integrity": "sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==", "dev": true, "license": "MIT", "dependencies": { @@ -7181,7 +7205,7 @@ "chalk": "5.0.1", "chalk-template": "0.4.0", "clipboardy": "3.0.0", - "compression": "1.7.4", + "compression": "1.8.1", "is-port-reachable": "4.0.0", "serve-handler": "6.1.6", "update-check": "1.5.4" @@ -8331,6 +8355,7 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } diff --git a/src/SupabaseClient.ts b/src/SupabaseClient.ts index 4fa749388..3acd10edf 100644 --- a/src/SupabaseClient.ts +++ b/src/SupabaseClient.ts @@ -371,6 +371,7 @@ export default class SupabaseClient< this.changedAccessToken !== token ) { this.changedAccessToken = token + this.realtime.setAuth(token) } else if (event === 'SIGNED_OUT') { this.realtime.setAuth() if (source == 'STORAGE') this.auth.signOut() diff --git a/supabase/.temp/cli-latest b/supabase/.temp/cli-latest index 8e00c6d6f..75788def2 100644 --- a/supabase/.temp/cli-latest +++ b/supabase/.temp/cli-latest @@ -1 +1 @@ -v2.33.9 \ No newline at end of file +v2.40.7 \ No newline at end of file diff --git a/test/deno/integration.test.ts b/test/deno/integration.test.ts index 4107ddb7b..7210fb32b 100644 --- a/test/deno/integration.test.ts +++ b/test/deno/integration.test.ts @@ -156,7 +156,6 @@ Deno.test( await supabase.auth.signUp({ email, password }) const config = { broadcast: { self: true }, private: true } channel = supabase.channel(channelName, { config }) - await supabase.realtime.setAuth() const testMessage = { message: 'test' } let receivedMessage: any diff --git a/test/integration.test.ts b/test/integration.test.ts index 55478ddcb..d9553405b 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -278,8 +278,6 @@ describe('Supabase Integration Tests', () => { const config = { broadcast: { self: true }, private: true } channel = supabase.channel(channelName, { config }) - - await supabase.realtime.setAuth() }) afterEach(async () => { diff --git a/test/integration/bun/bun.lockb b/test/integration/bun/bun.lockb new file mode 100755 index 000000000..f9fdf6ddd Binary files /dev/null and b/test/integration/bun/bun.lockb differ diff --git a/test/integration/bun/integration.test.ts b/test/integration/bun/integration.test.ts index 985a172c9..e35749229 100644 --- a/test/integration/bun/integration.test.ts +++ b/test/integration/bun/integration.test.ts @@ -14,7 +14,6 @@ test('should subscribe to realtime channel', async () => { const email = `bun-test-${Date.now()}@example.com` const password = 'password123' await supabase.auth.signUp({ email, password }) - await supabase.realtime.setAuth() const channelName = `bun-channel-${crypto.randomUUID()}` const config = { broadcast: { self: true }, private: true } diff --git a/test/unit/SupabaseClient.test.ts b/test/unit/SupabaseClient.test.ts index 5c82f446b..6f7304114 100644 --- a/test/unit/SupabaseClient.test.ts +++ b/test/unit/SupabaseClient.test.ts @@ -219,124 +219,174 @@ describe('SupabaseClient', () => { }) }) - describe('Access Token Handling', () => { - test('should use custom accessToken when provided', async () => { - const customToken = 'custom-jwt-token' - const client = createClient(URL, KEY, { - accessToken: async () => customToken, + describe('Token Management', () => { + describe('Token Resolution', () => { + test('should resolve token from session', async () => { + const expectedToken = 'test-jwt-token' + const client = createClient(URL, KEY) + + client.auth.getSession = jest.fn().mockResolvedValue({ + data: { session: { access_token: expectedToken } }, + }) + + // @ts-ignore - accessing private method + const token = await client._getAccessToken() + expect(token).toBe(expectedToken) }) - // Access the private method through the client instance - const accessToken = await (client as any)._getAccessToken() - expect(accessToken).toBe(customToken) - }) - - test('should fallback to session access token when no custom accessToken', async () => { - const client = createClient(URL, KEY) + test('should use custom accessToken callback', async () => { + const customToken = 'custom-access-token' + const customAccessTokenFn = jest.fn().mockResolvedValue(customToken) + const client = createClient(URL, KEY, { accessToken: customAccessTokenFn }) - // Mock the auth.getSession method - const mockSession = { - data: { - session: { - access_token: 'session-jwt-token', - }, - }, - } - client.auth.getSession = jest.fn().mockResolvedValue(mockSession) - - const accessToken = await (client as any)._getAccessToken() - expect(accessToken).toBe('session-jwt-token') - }) + // @ts-ignore - accessing private method + const token = await client._getAccessToken() + expect(token).toBe(customToken) + expect(customAccessTokenFn).toHaveBeenCalled() + }) - test('should fallback to supabaseKey when no session', async () => { - const client = createClient(URL, KEY) + test('should fallback to supabaseKey when no session available', async () => { + const client = createClient(URL, KEY) - // Mock the auth.getSession method to return no session - const mockSession = { - data: { - session: null, - }, - } - client.auth.getSession = jest.fn().mockResolvedValue(mockSession) + client.auth.getSession = jest.fn().mockResolvedValue({ + data: { session: null }, + }) - const accessToken = await (client as any)._getAccessToken() - expect(accessToken).toBe(KEY) + // @ts-ignore - accessing private method + const token = await client._getAccessToken() + expect(token).toBe(KEY) + }) }) - }) - describe('Auth Event Handling', () => { - test('should handle TOKEN_REFRESHED event', () => { - const client = createClient(URL, KEY) - const newToken = 'new-refreshed-token' + describe('Realtime Authentication', () => { + test('should provide access token to realtime client', async () => { + const expectedToken = 'test-jwt-token' + const client = createClient(URL, KEY) - // Mock realtime.setAuth - client.realtime.setAuth = jest.fn() - ;(client as any)._handleTokenChanged('TOKEN_REFRESHED', 'CLIENT', newToken) + client.auth.getSession = jest.fn().mockResolvedValue({ + data: { session: { access_token: expectedToken } }, + }) - expect((client as any).changedAccessToken).toBe(newToken) - }) + const realtimeToken = await client.realtime.accessToken!() + expect(realtimeToken).toBe(expectedToken) + }) - test('should listen for auth events', () => { - const client = createClient(URL, KEY) + test('should handle authentication state changes', async () => { + const client = createClient(URL, KEY) + const setAuthSpy = jest.spyOn(client.realtime, 'setAuth') - // Mock auth.onAuthStateChange - const mockCallback = jest.fn() - client.auth.onAuthStateChange = jest.fn().mockReturnValue(mockCallback) + // @ts-ignore - accessing private method for testing + client._handleTokenChanged('TOKEN_REFRESHED', 'CLIENT', 'new-token') + expect(setAuthSpy).toHaveBeenCalledWith('new-token') - // Call the private method - const result = (client as any)._listenForAuthEvents() + setAuthSpy.mockClear() - expect(client.auth.onAuthStateChange).toHaveBeenCalled() - expect(result).toBe(mockCallback) - }) + // @ts-ignore - accessing private method for testing + client._handleTokenChanged('SIGNED_IN', 'CLIENT', 'signin-token') + expect(setAuthSpy).toHaveBeenCalledWith('signin-token') - test('should handle SIGNED_IN event', () => { - const client = createClient(URL, KEY) - const newToken = 'new-signed-in-token' + setAuthSpy.mockClear() - // Mock realtime.setAuth - client.realtime.setAuth = jest.fn() - ;(client as any)._handleTokenChanged('SIGNED_IN', 'CLIENT', newToken) + // @ts-ignore - accessing private method for testing + client._handleTokenChanged('SIGNED_OUT', 'CLIENT') + expect(setAuthSpy).toHaveBeenCalledWith() + }) - expect((client as any).changedAccessToken).toBe(newToken) + test('should update token in realtime client when setAuth is called', async () => { + const client = createClient(URL, KEY) + const testToken = 'test-realtime-token' + + client.realtime.setAuth = jest.fn(async (token) => { + if (token) { + ;(client.realtime as any).accessTokenValue = token + } else { + const freshToken = await client.realtime.accessToken!() + ;(client.realtime as any).accessTokenValue = freshToken + } + }) + + await client.realtime.setAuth(testToken) + expect(client.realtime.setAuth).toHaveBeenCalledWith(testToken) + expect((client.realtime as any).accessTokenValue).toBe(testToken) + }) }) - test('should not update token if it is the same', () => { - const client = createClient(URL, KEY) - const existingToken = 'existing-token' - ;(client as any).changedAccessToken = existingToken + describe('FetchWithAuth Token Integration', () => { + test('should pass correct token to fetchWithAuth wrapper', async () => { + const expectedToken = 'test-fetch-token' + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({}), + }) - // Mock realtime.setAuth - client.realtime.setAuth = jest.fn() - ;(client as any)._handleTokenChanged('TOKEN_REFRESHED', 'CLIENT', existingToken) + const client = createClient(URL, KEY, { + global: { fetch: mockFetch }, + }) - expect((client as any).changedAccessToken).toBe(existingToken) - }) + client.auth.getSession = jest.fn().mockResolvedValue({ + data: { session: { access_token: expectedToken } }, + }) - test('should handle SIGNED_OUT event from CLIENT source', () => { - const client = createClient(URL, KEY) - ;(client as any).changedAccessToken = 'old-token' + await client.from('test').select('*') - // Mock realtime.setAuth - client.realtime.setAuth = jest.fn() - ;(client as any)._handleTokenChanged('SIGNED_OUT', 'CLIENT') + expect(mockFetch).toHaveBeenCalled() + const [, options] = mockFetch.mock.calls[0] + expect(options.headers.get('Authorization')).toBe(`Bearer ${expectedToken}`) + expect(options.headers.get('apikey')).toBe(KEY) + }) - expect(client.realtime.setAuth).toHaveBeenCalled() - expect((client as any).changedAccessToken).toBeUndefined() - }) + test('should work across all fetchWithAuth services', async () => { + const expectedToken = 'test-multi-service-token' + const mockFetch = jest + .fn() + .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}) }) // rest + .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: [] }) }) // storage + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve('{}'), + headers: new Map(), + }) // functions + + const client = createClient(URL, KEY, { + global: { fetch: mockFetch }, + }) + + client.auth.getSession = jest.fn().mockResolvedValue({ + data: { session: { access_token: expectedToken } }, + }) + + await client.from('test').select('*') + await client.storage.from('test').list() + await client.functions.invoke('test-function') + + expect(mockFetch).toHaveBeenCalledTimes(3) + + mockFetch.mock.calls.forEach(([, options]) => { + expect(options.headers.get('Authorization')).toBe(`Bearer ${expectedToken}`) + }) + }) - test('should handle SIGNED_OUT event from STORAGE source', () => { - const client = createClient(URL, KEY) - ;(client as any).changedAccessToken = 'old-token' + test('should use supabaseKey fallback in fetchWithAuth', async () => { + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({}), + }) - // Mock realtime.setAuth and auth.signOut - client.realtime.setAuth = jest.fn() - client.auth.signOut = jest.fn() - ;(client as any)._handleTokenChanged('SIGNED_OUT', 'STORAGE') + const client = createClient(URL, KEY, { + global: { fetch: mockFetch }, + }) - expect(client.realtime.setAuth).toHaveBeenCalled() - expect(client.auth.signOut).toHaveBeenCalled() - expect((client as any).changedAccessToken).toBeUndefined() + client.auth.getSession = jest.fn().mockResolvedValue({ + data: { session: null }, + }) + + await client.from('test').select('*') + + expect(mockFetch).toHaveBeenCalled() + const [, options] = mockFetch.mock.calls[0] + expect(options.headers.get('Authorization')).toBe(`Bearer ${KEY}`) + expect(options.headers.get('apikey')).toBe(KEY) + }) }) }) })