Ctrl K
ComunidadGuías…

tests/channel-device-override-kv.unit.test.ts

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

Archivo original ↗Preguntar a KiBot
import { beforeEach, describe, expect, it, vi } from 'vitest'

const checkPermissionMock = vi.fn()
const isValidAppIdMock = vi.fn()
const supabaseApikeyMock = vi.fn()
const supabaseWithAuthMock = vi.fn()
const syncLegacyChannelSelfOverrideDeleteForDeviceMock = vi.fn()
const syncLegacyChannelSelfOverrideForDeviceMock = vi.fn()
const updateOrCreateChannelDeviceMock = vi.fn()
const withRlsContextMock = vi.fn()

vi.mock('../supabase/functions/_backend/utils/channelSelfStore.ts', () => ({
  syncLegacyChannelSelfOverrideDeleteForDevice: syncLegacyChannelSelfOverrideDeleteForDeviceMock,
  syncLegacyChannelSelfOverrideForDevice: syncLegacyChannelSelfOverrideForDeviceMock,
}))

vi.mock('../supabase/functions/_backend/utils/hono.ts', () => ({
  BRES: { status: 'ok' },
  parseBody: vi.fn(),
  quickError: (status: number, error: string, message: string, details: Record<string, unknown> = {}) => {
    const issue = new Error(message)
    ;(issue as Error & { cause?: unknown }).cause = { status, error, ...details }
    throw issue
  },
  simpleError: (error: string, message: string, details: Record<string, unknown> = {}) => {
    const issue = new Error(message)
    ;(issue as Error & { cause?: unknown }).cause = { error, ...details }
    throw issue
  },
  useCors: async (_c: unknown, next: () => Promise<void>) => next(),
}))

vi.mock('../supabase/functions/_backend/utils/hono_middleware.ts', () => ({
  middlewareAuth: () => async (_c: unknown, next: () => Promise<void>) => next(),
}))

vi.mock('../supabase/functions/_backend/utils/rbac.ts', () => ({
  checkPermission: checkPermissionMock,
}))

vi.mock('../supabase/functions/_backend/utils/supabase.ts', () => ({
  supabaseApikey: supabaseApikeyMock,
  supabaseWithAuth: supabaseWithAuthMock,
  updateOrCreateChannelDevice: updateOrCreateChannelDeviceMock,
}))

vi.mock('../supabase/functions/_backend/utils/pg_rls.ts', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../supabase/functions/_backend/utils/pg_rls.ts')>()
  return {
    ...actual,
    withRlsContext: withRlsContextMock,
  }
})

vi.mock('../supabase/functions/_backend/utils/utils.ts', () => ({
  isValidAppId: isValidAppIdMock,
}))

function createContext() {
  return {
    get: vi.fn((key: string) => {
      if (key === 'auth') {
        return {
          apikey: null,
          authType: 'jwt',
          jwt: 'Bearer jwt-test',
          userId: 'user-test',
        }
      }
      if (key === 'requestId')
        return 'request-test'
      return undefined
    }),
    json: vi.fn((body: unknown) => body),
  }
}

function createChannelSelect(channel = { id: 42, app_id: 'com.test.app', owner_org: 'org-test', public: false }) {
  return {
    select() {
      return this
    },
    eq() {
      return this
    },
    single: vi.fn(async () => ({ data: channel, error: null })),
  }
}

function createDeviceSelect(pluginVersion: string | null = '7.33.0') {
  return {
    select() {
      return this
    },
    eq() {
      return this
    },
    maybeSingle: vi.fn(async () => ({
      data: pluginVersion ? { plugin_version: pluginVersion } : null,
      error: null,
    })),
  }
}

function createPublicApiSupabase(pluginVersion: string | null = '7.33.0') {
  return {
    from(table: string) {
      if (table === 'channels')
        return createChannelSelect()
      if (table === 'devices')
        return createDeviceSelect(pluginVersion)
      if (table === 'channel_devices') {
        return {
          delete() {
            return this
          },
          eq() {
            return this
          },
          then(resolve: (value: { error: null }) => unknown) {
            return Promise.resolve(resolve({ error: null }))
          },
        }
      }
      throw new Error(`Unexpected table: ${table}`)
    },
  }
}

// Direct-Postgres stand-in for the dashboard handlers: answers their SQL as the caller's RLS context would.
function createPrivateDb(
  channelDeviceOverride: { app_id: string, channel_id: number, owner_org: string } | null = { app_id: 'com.test.app', channel_id: 42, owner_org: 'org-test' },
  channel = { id: 42, app_id: 'com.test.app', owner_org: 'org-test', public: false },
) {
  const upsertMock = vi.fn()
  const deleteMock = vi.fn()
  const client = {
    query: vi.fn(async (text: string, values: unknown[] = []) => {
      if (text.startsWith('INSERT INTO public.channel_devices')) {
        const [app_id, channel_id, device_id, owner_org, is_self_set] = values
        upsertMock({ app_id, channel_id, device_id, owner_org, is_self_set })
        return { rows: [] }
      }
      if (text.startsWith('DELETE FROM public.channel_devices')) {
        deleteMock(values)
        return { rows: [] }
      }
      if (text.includes('FROM public.channels WHERE app_id = $1'))
        return { rows: channel ? [{ row: channel }] : [] }
      if (text.includes('FROM public.channel_devices WHERE device_id = $1 AND app_id = $2'))
        return { rows: channelDeviceOverride ? [{ row: channelDeviceOverride }] : [] }
      throw new Error(`Unexpected query: ${text}`)
    }),
  }
  withRlsContextMock.mockImplementation(async (_c: unknown, _principal: unknown, run: (client: unknown) => unknown) => run(client))
  return { client, upsertMock, deleteMock }
}

describe('channel device override KV sync', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    checkPermissionMock.mockResolvedValue(true)
    isValidAppIdMock.mockReturnValue(true)
    supabaseApikeyMock.mockReturnValue(createPublicApiSupabase())
    syncLegacyChannelSelfOverrideForDeviceMock.mockResolvedValue(true)
    syncLegacyChannelSelfOverrideDeleteForDeviceMock.mockResolvedValue(true)
    updateOrCreateChannelDeviceMock.mockResolvedValue({ error: null })
    createPrivateDb()
  })

  it('syncs public device API channel override writes through the legacy channel_self helper', async () => {
    const { post } = await import('../supabase/functions/_backend/public/device/post.ts')
    const c = createContext()

    await post(c as any, {
      app_id: 'com.test.app',
      channel: 'beta',
      device_id: '11111111-1111-4111-8111-111111111111',
    }, { key: 'capg-key' } as any)

    expect(updateOrCreateChannelDeviceMock).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
      app_id: 'com.test.app',
      channel_id: 42,
      device_id: '11111111-1111-4111-8111-111111111111',
      owner_org: 'org-test',
    }))
    expect(syncLegacyChannelSelfOverrideForDeviceMock).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
      app_id: 'com.test.app',
      channel_id: 42,
      device_id: '11111111-1111-4111-8111-111111111111',
    })
  })

  it('fails public device API channel override writes when KV sync fails', async () => {
    syncLegacyChannelSelfOverrideForDeviceMock.mockResolvedValue(false)
    const { post } = await import('../supabase/functions/_backend/public/device/post.ts')
    const c = createContext()

    await expect(post(c as any, {
      app_id: 'com.test.app',
      channel: 'beta',
      device_id: '11111111-1111-4111-8111-111111111111',
    }, { key: 'capg-key' } as any)).rejects.toThrow('Error syncing channel override store')
  })

  it('syncs public device API channel override deletes through the legacy channel_self helper', async () => {
    const { deleteOverride } = await import('../supabase/functions/_backend/public/device/delete.ts')
    const c = createContext()

    await deleteOverride(c as any, {
      app_id: 'com.test.app',
      device_id: '11111111-1111-4111-8111-111111111111',
    }, { key: 'capg-key' } as any)

    expect(syncLegacyChannelSelfOverrideDeleteForDeviceMock).toHaveBeenCalledWith(
      expect.anything(),
      expect.anything(),
      'com.test.app',
      '11111111-1111-4111-8111-111111111111',
    )
  })

  it('fails public device API channel override deletes when KV sync fails', async () => {
    syncLegacyChannelSelfOverrideDeleteForDeviceMock.mockResolvedValue(false)
    const { deleteOverride } = await import('../supabase/functions/_backend/public/device/delete.ts')
    const c = createContext()

    await expect(deleteOverride(c as any, {
      app_id: 'com.test.app',
      device_id: '11111111-1111-4111-8111-111111111111',
    }, { key: 'capg-key' } as any)).rejects.toThrow('Error syncing channel override store')
  })

  it('syncs dashboard channel override writes through the legacy channel_self helper', async () => {
    const privateSupabase = createPrivateDb()
    const { setChannelDeviceOverride } = await import('../supabase/functions/_backend/private/channel_device.ts')
    const c = createContext()

    await setChannelDeviceOverride(c as any, {
      app_id: 'com.test.app',
      channel_id: 42,
      device_id: '11111111-1111-4111-8111-111111111111',
    })

    expect(checkPermissionMock).toHaveBeenCalledWith(expect.anything(), 'channel.manage_forced_devices', {
      appId: 'com.test.app',
      channelId: 42,
    })

    expect(privateSupabase.upsertMock).toHaveBeenCalledWith({
      app_id: 'com.test.app',
      channel_id: 42,
      device_id: '11111111-1111-4111-8111-111111111111',
      owner_org: 'org-test',
      is_self_set: false,
    })
    expect(syncLegacyChannelSelfOverrideForDeviceMock).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
      app_id: 'com.test.app',
      channel_id: 42,
      device_id: '11111111-1111-4111-8111-111111111111',
      owner_org: 'org-test',
      is_self_set: false,
    })
  })

  it('rejects dashboard channel override writes without channel forced-device permission', async () => {
    const privateSupabase = createPrivateDb()
    checkPermissionMock.mockResolvedValue(false)
    const { setChannelDeviceOverride } = await import('../supabase/functions/_backend/private/channel_device.ts')
    const c = createContext()

    await expect(setChannelDeviceOverride(c as any, {
      app_id: 'com.test.app',
      channel_id: 42,
      device_id: '11111111-1111-4111-8111-111111111111',
    })).rejects.toThrow('Permission denied: channel.manage_forced_devices')

    expect(privateSupabase.upsertMock).not.toHaveBeenCalled()
    expect(syncLegacyChannelSelfOverrideForDeviceMock).not.toHaveBeenCalled()
  })

  it('fails dashboard channel override writes when KV sync fails', async () => {
    createPrivateDb()
    syncLegacyChannelSelfOverrideForDeviceMock.mockResolvedValue(false)
    const { setChannelDeviceOverride } = await import('../supabase/functions/_backend/private/channel_device.ts')
    const c = createContext()

    await expect(setChannelDeviceOverride(c as any, {
      app_id: 'com.test.app',
      channel_id: 42,
      device_id: '11111111-1111-4111-8111-111111111111',
    })).rejects.toThrow('Error syncing channel override store')
  })

  it('syncs dashboard channel override deletes through the legacy channel_self helper', async () => {
    const privateSupabase = createPrivateDb()
    const { deleteChannelDeviceOverride } = await import('../supabase/functions/_backend/private/channel_device.ts')
    const c = createContext()

    await deleteChannelDeviceOverride(c as any, {
      app_id: 'com.test.app',
      device_id: '11111111-1111-4111-8111-111111111111',
    })

    expect(privateSupabase.deleteMock).toHaveBeenCalledWith(['11111111-1111-4111-8111-111111111111', 'com.test.app'])
    expect(checkPermissionMock).toHaveBeenCalledWith(expect.anything(), 'channel.manage_forced_devices', {
      appId: 'com.test.app',
      channelId: 42,
    })

    expect(syncLegacyChannelSelfOverrideDeleteForDeviceMock).toHaveBeenCalledWith(
      expect.anything(),
      expect.anything(),
      'com.test.app',
      '11111111-1111-4111-8111-111111111111',
    )
  })

  it('fails dashboard channel override deletes when KV sync fails', async () => {
    createPrivateDb()
    syncLegacyChannelSelfOverrideDeleteForDeviceMock.mockResolvedValue(false)
    const { deleteChannelDeviceOverride } = await import('../supabase/functions/_backend/private/channel_device.ts')
    const c = createContext()

    await expect(deleteChannelDeviceOverride(c as any, {
      app_id: 'com.test.app',
      device_id: '11111111-1111-4111-8111-111111111111',
    })).rejects.toThrow('Error syncing channel override store')
  })
})