393 lines
14 KiB
TypeScript
Executable File
393 lines
14 KiB
TypeScript
Executable File
import { PostgrestClient } from 'npm:@supabase/postgrest-js@1.21.4'
|
|
import { createClient, SupabaseClient } from '../../src/index.ts'
|
|
import { Database } from '../types.ts'
|
|
|
|
const URL = 'http://localhost:3000'
|
|
const KEY = 'some.fake.key'
|
|
|
|
describe('SupabaseClient', () => {
|
|
test('it should create a client with third-party auth accessToken', async () => {
|
|
const client = createClient(URL, KEY, {
|
|
accessToken: async () => {
|
|
return 'jwt'
|
|
},
|
|
})
|
|
|
|
expect(() => client.auth.getUser()).toThrow(
|
|
'@supabase/supabase-js: Supabase Client is configured with the accessToken option, accessing supabase.auth.getUser is not possible'
|
|
)
|
|
})
|
|
|
|
test('it should create the client connection', async () => {
|
|
const supabase = createClient(URL, KEY)
|
|
expect(supabase).toBeDefined()
|
|
expect(supabase).toBeInstanceOf(SupabaseClient)
|
|
})
|
|
|
|
test('it should throw an error if no valid params are provided', async () => {
|
|
expect(() => createClient('', KEY)).toThrow('supabaseUrl is required.')
|
|
expect(() => createClient(URL, '')).toThrow('supabaseKey is required.')
|
|
})
|
|
|
|
test('should validate supabaseUrl', () => {
|
|
expect(() => createClient('https://xyz123.supabase.co', KEY)).not.toThrow()
|
|
expect(() => createClient('http://localhost:54321', KEY)).not.toThrow()
|
|
expect(() => createClient('http://[invalid', KEY)).toThrow(
|
|
'Invalid supabaseUrl: Provided URL is malformed.'
|
|
)
|
|
expect(() =>
|
|
createClient('postgresql://postgre:password@db.test.co:5432/postgres', KEY)
|
|
).toThrow('Invalid supabaseUrl: Must be a valid HTTP or HTTPS URL.')
|
|
expect(() => createClient('http:/localhost:3000', KEY)).toThrow(
|
|
'Invalid supabaseUrl: Must be a valid HTTP or HTTPS URL.'
|
|
)
|
|
|
|
expect(() => createClient(' https://xyz123.supabase.co ', KEY)).not.toThrow()
|
|
expect(() => createClient('http://user:pass@localhost:54321', KEY)).not.toThrow()
|
|
})
|
|
|
|
describe('URL Construction', () => {
|
|
test('should construct URLs correctly', () => {
|
|
const client = createClient(URL, KEY)
|
|
|
|
// @ts-ignore
|
|
expect(client.authUrl.toString()).toEqual('http://localhost:3000/auth/v1')
|
|
// @ts-ignore
|
|
expect(client.realtimeUrl.toString()).toEqual('ws://localhost:3000/realtime/v1')
|
|
// @ts-ignore
|
|
expect(client.storageUrl.toString()).toEqual('http://localhost:3000/storage/v1')
|
|
// @ts-ignore
|
|
expect(client.functionsUrl.toString()).toEqual('http://localhost:3000/functions/v1')
|
|
// @ts-ignore
|
|
expect(client.rest.url).toEqual('http://localhost:3000/rest/v1')
|
|
})
|
|
|
|
test('should preserve paths in supabaseUrl', () => {
|
|
const baseUrlWithPath = 'http://localhost:3000/custom/base'
|
|
const client = createClient(baseUrlWithPath, KEY)
|
|
|
|
// @ts-ignore
|
|
expect(client.authUrl.toString()).toEqual('http://localhost:3000/custom/base/auth/v1')
|
|
// @ts-ignore
|
|
expect(client.realtimeUrl.toString()).toEqual('ws://localhost:3000/custom/base/realtime/v1')
|
|
// @ts-ignore
|
|
expect(client.storageUrl.toString()).toEqual('http://localhost:3000/custom/base/storage/v1')
|
|
// @ts-ignore
|
|
expect(client.functionsUrl.toString()).toEqual(
|
|
'http://localhost:3000/custom/base/functions/v1'
|
|
)
|
|
// @ts-ignore
|
|
expect(client.rest.url).toEqual('http://localhost:3000/custom/base/rest/v1')
|
|
})
|
|
|
|
test('should handle HTTPS URLs correctly', () => {
|
|
const client = createClient('https://localhost:3000', KEY)
|
|
// @ts-ignore
|
|
expect(client.realtimeUrl.toString()).toEqual('wss://localhost:3000/realtime/v1')
|
|
})
|
|
})
|
|
|
|
describe('Custom Headers', () => {
|
|
test('should have custom header set', () => {
|
|
const customHeader = { 'X-Test-Header': 'value' }
|
|
const request = createClient(URL, KEY, { global: { headers: customHeader } }).rpc('')
|
|
//@ts-expect-error headers is protected attribute
|
|
const requestHeader = request.headers.get('X-Test-Header')
|
|
expect(requestHeader).toBe(customHeader['X-Test-Header'])
|
|
})
|
|
|
|
test('should merge custom headers with default headers', () => {
|
|
const customHeader = { 'X-Test-Header': 'value' }
|
|
const request = createClient(URL, KEY, { global: { headers: customHeader } }).rpc('')
|
|
|
|
//@ts-expect-error headers is protected attribute
|
|
const requestHeader = request.headers.get('X-Test-Header')
|
|
expect(requestHeader).toBe(customHeader['X-Test-Header'])
|
|
//@ts-expect-error headers is protected attribute
|
|
expect(request.headers.get('X-Client-Info')).not.toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Storage Key', () => {
|
|
test('should use default storage key based on project ref', () => {
|
|
const client = createClient('https://project-ref.supabase.co', KEY)
|
|
// @ts-ignore
|
|
expect(client.storageKey).toBe('sb-project-ref-auth-token')
|
|
})
|
|
|
|
test('should use custom storage key when provided', () => {
|
|
const customStorageKey = 'custom-storage-key'
|
|
const client = createClient(URL, KEY, {
|
|
auth: { storageKey: customStorageKey },
|
|
})
|
|
// @ts-ignore
|
|
expect(client.storageKey).toBe(customStorageKey)
|
|
})
|
|
|
|
test('should handle undefined storageKey and headers', () => {
|
|
const client = createClient(URL, KEY, {
|
|
auth: { storageKey: undefined },
|
|
global: { headers: undefined },
|
|
})
|
|
expect(client).toBeDefined()
|
|
// @ts-ignore
|
|
expect(client.storageKey).toBe('')
|
|
// @ts-ignore
|
|
expect(client.headers).toHaveProperty('X-Client-Info')
|
|
})
|
|
})
|
|
|
|
describe('Client Methods', () => {
|
|
test('should initialize functions client', () => {
|
|
const client = createClient(URL, KEY)
|
|
const functions = client.functions
|
|
expect(functions).toBeDefined()
|
|
// @ts-ignore
|
|
expect(functions.url).toBe('http://localhost:3000/functions/v1')
|
|
})
|
|
|
|
test('should initialize storage client', () => {
|
|
const client = createClient(URL, KEY)
|
|
const storage = client.storage
|
|
expect(storage).toBeDefined()
|
|
// @ts-ignore
|
|
expect(storage.url).toBe('http://localhost:3000/storage/v1')
|
|
})
|
|
|
|
test('should initialize realtime client', () => {
|
|
const client = createClient(URL, KEY)
|
|
expect(client.realtime).toBeDefined()
|
|
// @ts-ignore
|
|
expect(client.realtime.endPoint).toBe('ws://localhost:3000/realtime/v1/websocket')
|
|
})
|
|
})
|
|
|
|
describe('Realtime Channel Management', () => {
|
|
test('should create and manage channels', () => {
|
|
const client = createClient(URL, KEY)
|
|
const channel = client.channel('test-channel')
|
|
expect(channel).toBeDefined()
|
|
expect(client.getChannels()).toHaveLength(1)
|
|
})
|
|
|
|
test('should remove channel', async () => {
|
|
const client = createClient(URL, KEY)
|
|
const channel = client.channel('test-channel')
|
|
const result = await client.removeChannel(channel)
|
|
expect(result).toBe('ok')
|
|
expect(client.getChannels()).toHaveLength(0)
|
|
})
|
|
|
|
test('should remove all channels', async () => {
|
|
const client = createClient(URL, KEY)
|
|
client.channel('channel1')
|
|
client.channel('channel2')
|
|
const results = await client.removeAllChannels()
|
|
expect(results).toEqual(['ok', 'ok'])
|
|
expect(client.getChannels()).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe('Schema Management', () => {
|
|
test('should switch schema', () => {
|
|
const client = createClient<Database>(URL, KEY)
|
|
const schemaClient = client.schema('personal')
|
|
expect(schemaClient).toBeDefined()
|
|
expect(schemaClient).toBeInstanceOf(PostgrestClient)
|
|
})
|
|
})
|
|
|
|
describe('Table/View Queries', () => {
|
|
test('should query table with string relation', () => {
|
|
const client = createClient<Database>(URL, KEY)
|
|
const queryBuilder = client.from('users')
|
|
expect(queryBuilder).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('RPC Calls', () => {
|
|
test('should make RPC call with arguments', () => {
|
|
const client = createClient<Database>(URL, KEY)
|
|
const rpcCall = client.rpc('get_status', { name_param: 'test' })
|
|
expect(rpcCall).toBeDefined()
|
|
})
|
|
|
|
test('should make RPC call with options', () => {
|
|
const client = createClient<Database>(URL, KEY)
|
|
const rpcCall = client.rpc('get_status', { name_param: 'test' }, { head: true })
|
|
expect(rpcCall).toBeDefined()
|
|
})
|
|
})
|
|
|
|
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)
|
|
})
|
|
|
|
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 })
|
|
|
|
// @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 available', async () => {
|
|
const client = createClient(URL, KEY)
|
|
|
|
client.auth.getSession = jest.fn().mockResolvedValue({
|
|
data: { session: null },
|
|
})
|
|
|
|
// @ts-ignore - accessing private method
|
|
const token = await client._getAccessToken()
|
|
expect(token).toBe(KEY)
|
|
})
|
|
})
|
|
|
|
describe('Realtime Authentication', () => {
|
|
test('should provide access token to realtime client', async () => {
|
|
const expectedToken = 'test-jwt-token'
|
|
const client = createClient(URL, KEY)
|
|
|
|
client.auth.getSession = jest.fn().mockResolvedValue({
|
|
data: { session: { access_token: expectedToken } },
|
|
})
|
|
|
|
const realtimeToken = await client.realtime.accessToken!()
|
|
expect(realtimeToken).toBe(expectedToken)
|
|
})
|
|
|
|
test('should handle authentication state changes', async () => {
|
|
const client = createClient(URL, KEY)
|
|
const setAuthSpy = jest.spyOn(client.realtime, 'setAuth')
|
|
|
|
// @ts-ignore - accessing private method for testing
|
|
client._handleTokenChanged('TOKEN_REFRESHED', 'CLIENT', 'new-token')
|
|
expect(setAuthSpy).toHaveBeenCalledWith('new-token')
|
|
|
|
setAuthSpy.mockClear()
|
|
|
|
// @ts-ignore - accessing private method for testing
|
|
client._handleTokenChanged('SIGNED_IN', 'CLIENT', 'signin-token')
|
|
expect(setAuthSpy).toHaveBeenCalledWith('signin-token')
|
|
|
|
setAuthSpy.mockClear()
|
|
|
|
// @ts-ignore - accessing private method for testing
|
|
client._handleTokenChanged('SIGNED_OUT', 'CLIENT')
|
|
expect(setAuthSpy).toHaveBeenCalledWith()
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|
|
|
|
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({}),
|
|
})
|
|
|
|
const client = createClient(URL, KEY, {
|
|
global: { fetch: mockFetch },
|
|
})
|
|
|
|
client.auth.getSession = jest.fn().mockResolvedValue({
|
|
data: { session: { access_token: expectedToken } },
|
|
})
|
|
|
|
await client.from('test').select('*')
|
|
|
|
expect(mockFetch).toHaveBeenCalled()
|
|
const [, options] = mockFetch.mock.calls[0]
|
|
expect(options.headers.get('Authorization')).toBe(`Bearer ${expectedToken}`)
|
|
expect(options.headers.get('apikey')).toBe(KEY)
|
|
})
|
|
|
|
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 use supabaseKey fallback in fetchWithAuth', async () => {
|
|
const mockFetch = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({}),
|
|
})
|
|
|
|
const client = createClient(URL, KEY, {
|
|
global: { fetch: mockFetch },
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|
|
})
|
|
})
|