Ctrl K
ComunidadGuías…

tests/channel-self-pg-client.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 getPgClientMock = vi.fn(() => ({ client: 'pg' }))
const getDrizzleClientMock = vi.fn(() => ({}))
const deleteChannelDevicePgMock = vi.fn(() => Promise.resolve(true))
const getAppOwnerPostgresMock = vi.fn()
const getChannelByIdPgMock = vi.fn()
const getChannelByNamePgMock = vi.fn()
const getChannelsPgMock = vi.fn()
const getChannelDeviceOverridePgMock = vi.fn()
const upsertChannelDevicePgMock = vi.fn(() => Promise.resolve(true))

;(globalThis as any).EdgeRuntime = undefined

vi.mock('../supabase/functions/_backend/plugin_runtime/utils/appStatus.ts', () => ({
  getAppStatus: vi.fn(() => Promise.resolve({ status: 'cloud', allow_device_custom_id: true })),
  setAppStatus: vi.fn(() => Promise.resolve()),
}))

vi.mock('../supabase/functions/_backend/plugin_runtime/utils/channelSelfRateLimit.ts', () => ({
  checkChannelSelfIPRateLimit: vi.fn(() => Promise.resolve({ limited: false })),
  isChannelSelfRateLimited: vi.fn(() => Promise.resolve({ limited: false })),
  recordChannelSelfIPRequest: vi.fn(() => Promise.resolve()),
  recordChannelSelfRequest: vi.fn(() => Promise.resolve()),
}))

vi.mock('../supabase/functions/_backend/plugin_runtime/utils/discord.ts', () => ({
  sendDiscordAlert500: vi.fn(() => Promise.resolve()),
  sendDiscordAlert: vi.fn(() => Promise.resolve()),
}))

vi.mock('../supabase/functions/_backend/plugin_runtime/utils/notifications.ts', () => ({
  sendNotifOrgCached: vi.fn(() => Promise.resolve()),
}))

vi.mock('../supabase/functions/_backend/plugin_runtime/utils/org_email_notifications.ts', () => ({
  sendNotifToOrgMembersCached: vi.fn(() => Promise.resolve()),
}))

vi.mock('../supabase/functions/_backend/plugin_runtime/utils/pg.ts', () => ({
  closeClient: vi.fn(() => Promise.resolve()),
  deleteChannelDevicePg: deleteChannelDevicePgMock,
  getAppByIdPg: vi.fn(),
  getAppOwnerPostgres: getAppOwnerPostgresMock,
  getChannelByIdPg: getChannelByIdPgMock,
  getChannelByNamePg: getChannelByNamePgMock,
  getChannelDeviceOverridePg: getChannelDeviceOverridePgMock,
  getChannelsPg: getChannelsPgMock,
  getCompatibleChannelsPg: vi.fn(),
  getDrizzleClient: getDrizzleClientMock,
  getMainChannelsPg: vi.fn(),
  getPgClient: getPgClientMock,
  setReplicationLagHeader: vi.fn(() => Promise.resolve()),
  upsertChannelDevicePg: upsertChannelDevicePgMock,
}))

vi.mock('../supabase/functions/_backend/plugin_runtime/utils/plugin_stats.ts', () => ({
  sendStatsAndDevice: vi.fn(() => Promise.resolve()),
}))

function putBody(pluginVersion: string) {
  return {
    app_id: 'com.test.app',
    device_id: '11111111-1111-4111-8111-111111111111',
    platform: 'ios',
    version_name: '1.0.0',
    version_build: '1.0.0',
    is_emulator: false,
    is_prod: true,
    plugin_version: pluginVersion,
  }
}

function createKvStore() {
  const values = new Map<string, string>()
  return {
    values,
    get: vi.fn(async (key: string, options?: { type?: string }) => {
      const value = values.get(key)
      if (!value)
        return null
      return options?.type === 'json' ? JSON.parse(value) : value
    }),
    put: vi.fn(async (key: string, value: string) => {
      values.set(key, value)
    }),
    delete: vi.fn(async (key: string) => {
      values.delete(key)
    }),
  }
}

function channelSelfStoreKey() {
  return 'channel_self:v1:com.test.app:11111111-1111-4111-8111-111111111111'
}

async function fetchPut(pluginVersion: string, env = {}) {
  const { app } = await import('../supabase/functions/_backend/plugin_runtime/plugins/channel_self.ts')
  return app.fetch(new Request('http://localhost/', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(putBody(pluginVersion)),
  }), env, { waitUntil: () => { } } as any)
}

async function fetchPost(pluginVersion: string, env = {}) {
  const { app } = await import('../supabase/functions/_backend/plugin_runtime/plugins/channel_self.ts')
  return app.fetch(new Request('http://localhost/', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      ...putBody(pluginVersion),
      channel: 'beta',
    }),
  }), env, { waitUntil: () => { } } as any)
}

async function fetchDelete(pluginVersion: string, env = {}) {
  const { app } = await import('../supabase/functions/_backend/plugin_runtime/plugins/channel_self.ts')
  const params = new URLSearchParams()
  for (const [key, value] of Object.entries(putBody(pluginVersion))) {
    params.set(key, String(value))
  }
  return app.fetch(new Request(`http://localhost/?${params.toString()}`, {
    method: 'DELETE',
  }), env, { waitUntil: () => { } } as any)
}

describe('channel_self PUT database routing', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    getAppOwnerPostgresMock.mockResolvedValue({
      allow_device_custom_id: true,
      owner_org: 'test-org',
      orgs: { management_email: 'owner@example.com' },
      plan_valid: true,
    })
    getChannelsPgMock.mockResolvedValue([
      {
        android: true,
        electron: true,
        ios: true,
        name: 'production',
      },
    ])
    getChannelDeviceOverridePgMock.mockResolvedValue(null)
    getChannelByNamePgMock.mockResolvedValue({
      id: 12,
      name: 'beta',
      allow_device_self_set: true,
      public: false,
      owner_org: 'test-org',
    })
    getChannelByIdPgMock.mockResolvedValue({
      id: 12,
      name: 'beta-current',
      allow_device_self_set: true,
      public: false,
      owner_org: 'test-org',
    })
  })

  it('uses primary for old plugin channel_devices read-after-write consistency', async () => {
    const response = await fetchPut('7.33.0')

    expect(response.status).toBe(200)
    expect(getPgClientMock).toHaveBeenCalledWith(expect.anything(), false)
  })

  it('uses replica for new plugin local channel storage reads', async () => {
    const response = await fetchPut('7.34.0')

    expect(response.status).toBe(200)
    expect(getPgClientMock).toHaveBeenCalledWith(expect.anything(), true)
  })

  it('uses replica and KV for old plugin reads when channel self store is bound', async () => {
    const kv = createKvStore()
    const response = await fetchPut('7.33.0', { CHANNEL_SELF_STORE: kv })

    expect(response.status).toBe(200)
    expect(getPgClientMock).toHaveBeenCalledWith(expect.anything(), true)
    expect(getChannelDeviceOverridePgMock).not.toHaveBeenCalled()
    expect(kv.get).toHaveBeenCalled()
  })

  it('resolves KV channel id through current channel metadata for old plugin reads', async () => {
    const kv = createKvStore()
    kv.values.set(channelSelfStoreKey(), JSON.stringify({
      app_id: 'com.test.app',
      device_id: '11111111-1111-4111-8111-111111111111',
      channel_id: 12,
      channel_name: 'stale-name',
      allow_device_self_set: false,
      updated_at: '2026-01-01T00:00:00.000Z',
    }))

    const response = await fetchPut('7.33.0', { CHANNEL_SELF_STORE: kv })

    expect(response.status).toBe(200)
    expect(await response.json()).toMatchObject({
      channel: 'beta-current',
      status: 'override',
      allowSet: true,
    })
    expect(getChannelByIdPgMock).toHaveBeenCalledWith(expect.anything(), 'com.test.app', 12, expect.anything())
  })

  it('uses replica and KV for old plugin writes when channel self store is bound', async () => {
    const kv = createKvStore()
    const response = await fetchPost('7.33.0', { CHANNEL_SELF_STORE: kv })

    expect(response.status).toBe(200)
    expect(getPgClientMock).toHaveBeenCalledWith(expect.anything(), true)
    expect(upsertChannelDevicePgMock).not.toHaveBeenCalled()
    expect(kv.put).toHaveBeenCalled()
    expect(JSON.parse(kv.put.mock.calls[0][1])).toEqual({
      app_id: 'com.test.app',
      device_id: '11111111-1111-4111-8111-111111111111',
      channel_id: 12,
      updated_at: expect.any(String),
    })
  })

  it('does not query KV for new plugin channel self storage when store is bound', async () => {
    const putKv = createKvStore()
    const putResponse = await fetchPut('7.34.0', { CHANNEL_SELF_STORE: putKv })

    expect(putResponse.status).toBe(200)
    expect(putKv.get).not.toHaveBeenCalled()
    expect(putKv.put).not.toHaveBeenCalled()
    expect(putKv.delete).not.toHaveBeenCalled()

    const postKv = createKvStore()
    const postResponse = await fetchPost('7.34.0', { CHANNEL_SELF_STORE: postKv })

    expect(postResponse.status).toBe(200)
    expect(postKv.get).not.toHaveBeenCalled()
    expect(postKv.put).not.toHaveBeenCalled()
    expect(postKv.delete).not.toHaveBeenCalled()

    const deleteKv = createKvStore()
    const deleteResponse = await fetchDelete('7.34.0', { CHANNEL_SELF_STORE: deleteKv })

    expect(deleteResponse.status).toBe(200)
    expect(deleteKv.get).not.toHaveBeenCalled()
    expect(deleteKv.put).not.toHaveBeenCalled()
    expect(deleteKv.delete).not.toHaveBeenCalled()
  })
})