Ctrl K
ComunidadGuías…

tests/console-client.unit.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { SESSION_STORAGE_KEY } from '../src/services/consoleAuth'
import { createConsoleClient, FunctionsFetchError, FunctionsHttpError } from '../src/services/consoleClient'
import { fakeJwt, memoryStorage } from './console-auth-fixtures'

const fetchMock = vi.fn()

function client(token: string | null = 'session-jwt') {
  const storage = memoryStorage()
  if (token)
    storage.setItem(SESSION_STORAGE_KEY, JSON.stringify({ access_token: fakeJwt({ sub: 'user-1' }), refresh_token: 'refresh', expires_at: Math.floor(Date.now() / 1000) + 3600, user: { id: 'user-1' } }))
  vi.stubGlobal('localStorage', storage)
  return createConsoleClient()
}

function jsonResponse(body: unknown, status = 200) {
  return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } })
}

beforeEach(() => {
  vi.stubEnv('VITE_API_HOST', 'https://api.example.com')
  vi.stubGlobal('fetch', fetchMock)
  fetchMock.mockReset()
})
afterEach(() => {
  vi.unstubAllEnvs()
  vi.unstubAllGlobals()
})

describe('console client transport', () => {
  it('replays a recorded table query through the Capgo API with the session bearer', async () => {
    fetchMock.mockResolvedValue(jsonResponse({ data: [{ id: 'app-1' }], error: null, count: 1, status: 200 }))
    const result = await client().from('apps').select('id', { count: 'exact' }).eq('owner_org', 'org-1').order('name').limit(10)

    expect(result).toEqual({ data: [{ id: 'app-1' }], error: null, count: 1, status: 200 })
    const [url, init] = fetchMock.mock.calls[0]
    expect(url).toBe('https://api.example.com/private/console/query')
    expect(new Headers(init.headers).get('Authorization')).toBe(`Bearer ${fakeJwt({ sub: 'user-1' })}`)
    expect(JSON.parse(init.body)).toEqual({
      kind: 'table',
      name: 'apps',
      args: [],
      operations: [
        { method: 'select', args: ['id', { count: 'exact' }] },
        { method: 'eq', args: ['owner_org', 'org-1'] },
        { method: 'order', args: ['name'] },
        { method: 'limit', args: [10] },
      ],
    })
  })

  it('drops trailing undefined arguments so server defaults still apply', async () => {
    fetchMock.mockResolvedValue(jsonResponse({ data: [], error: null, count: null, status: 200 }))
    await client().from('apps').select('id', undefined as any).order('name', undefined as any).range(0, 9)
    const [, init] = fetchMock.mock.calls[0]
    expect(JSON.parse(init.body).operations).toEqual([
      { method: 'select', args: ['id'] },
      { method: 'order', args: ['name'] },
      { method: 'range', args: [0, 9] },
    ])
  })

  it('sends rpc arguments and forwards the abort signal', async () => {
    fetchMock.mockResolvedValue(jsonResponse({ data: true, error: null, count: null, status: 200 }))
    const controller = new AbortController()
    const result = await client().rpc('is_paying_org', { orgid: 'org-1' }).abortSignal(controller.signal)

    expect(result.data).toBe(true)
    const [, init] = fetchMock.mock.calls[0]
    expect(init.signal).toBe(controller.signal)
    expect(JSON.parse(init.body)).toMatchObject({ kind: 'rpc', name: 'is_paying_org', args: [{ orgid: 'org-1' }] })
  })

  it('keeps PostgREST error details so callers can match codes', async () => {
    fetchMock.mockResolvedValue(jsonResponse({ data: null, error: { message: 'duplicate key', code: '23505', details: null, hint: null }, count: null, status: 409 }))
    const result = await client().from('apps').insert({ app_id: 'dup' } as any)
    expect(result.data).toBeNull()
    expect(result.error).toBeInstanceOf(Error)
    expect(result.error).toMatchObject({ message: 'duplicate key', code: '23505' })
  })

  it('throws on throwOnError like the SDK', async () => {
    fetchMock.mockResolvedValue(jsonResponse({ data: null, error: { message: 'denied', code: '42501' }, count: null, status: 403 }))
    await expect(client().from('apps').select('id').throwOnError()).rejects.toMatchObject({ code: '42501' })
  })

  it('surfaces API failures as FunctionsHttpError and network failures as FunctionsFetchError', async () => {
    fetchMock.mockResolvedValueOnce(jsonResponse({ error: 'unsupported_query' }, 403))
    const denied = await client().from('secret_table' as any).select('*')
    expect(denied.error).toBeInstanceOf(FunctionsHttpError)
    expect(denied.status).toBe(403)

    fetchMock.mockRejectedValueOnce(new TypeError('Failed to fetch'))
    const offline = await client().functions.invoke('private/anything', { body: { a: 1 } })
    expect(offline.error).toBeInstanceOf(FunctionsFetchError)
  })

  it('retries read queries after a network drop or a 503, never mutations', async () => {
    fetchMock
      .mockRejectedValueOnce(new TypeError('Failed to fetch'))
      .mockResolvedValueOnce(jsonResponse({ error: 'booting' }, 503))
      .mockResolvedValueOnce(jsonResponse({ data: [{ id: 'org-1' }], error: null, count: null, status: 200 }))
    const read = await client().rpc('get_orgs_v7')
    expect(read.data).toEqual([{ id: 'org-1' }])
    expect(fetchMock).toHaveBeenCalledTimes(3)

    fetchMock.mockReset()
    fetchMock.mockResolvedValueOnce(jsonResponse({ error: 'booting' }, 503))
    const write = await client().from('apps').update({ name: 'x' } as any).eq('id', 'app-1')
    expect(write.error).toBeInstanceOf(FunctionsHttpError)
    expect(fetchMock).toHaveBeenCalledTimes(1)

    // An RPC that mutates is never replayed either, whatever its arguments.
    fetchMock.mockReset()
    fetchMock.mockResolvedValueOnce(jsonResponse({ error: 'booting' }, 503))
    const rpcWrite = await client().rpc('transfer_app', { p_app_id: 'app-1' } as any)
    expect(rpcWrite.error).toBeInstanceOf(FunctionsHttpError)
    expect(fetchMock).toHaveBeenCalledTimes(1)
  })

  it('omits the bearer when there is no session', async () => {
    fetchMock.mockResolvedValue(jsonResponse({ data: true, error: null, count: null, status: 200 }))
    await client(null).rpc('is_not_deleted', { email_check: 'a@b.c' })
    const [, init] = fetchMock.mock.calls[0]
    expect(new Headers(init.headers).has('Authorization')).toBe(false)
  })

  it('routes storage calls to the image endpoints with raw bytes for uploads', async () => {
    fetchMock.mockResolvedValueOnce(jsonResponse({ data: { signedUrl: 'https://api.example.com/private/console/images/read?path=a&token=t' }, error: null }))
    const signed = await client().storage.from('images').createSignedUrl('org/logo.png', 60)
    expect(signed.data?.signedUrl).toContain('/private/console/images/read')
    expect(fetchMock.mock.calls[0][0]).toBe('https://api.example.com/private/console/images/sign')

    fetchMock.mockResolvedValueOnce(jsonResponse({ data: { path: 'org/logo.png' }, error: null }))
    const blob = new Blob(['png-bytes'], { type: 'image/png' })
    await client().storage.from('images').upload('org/logo.png', blob, { contentType: 'image/png', upsert: true })
    const [url, init] = fetchMock.mock.calls[1]
    expect(url).toBe('https://api.example.com/private/console/images/upload')
    expect(init.body).toBe(blob)
    const headers = new Headers(init.headers)
    expect(headers.get('x-image-path')).toBe('org/logo.png')
    expect(headers.get('x-image-upsert')).toBe('true')
    expect(headers.get('Content-Type')).toBe('image/png')

    expect(() => client().storage.from('other')).toThrow('Unsupported console storage bucket')
  })
})