Ctrl K
ComunidadGuías…

tests/bundle.test.ts

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

Archivo original ↗Preguntar a KiBot
import { randomUUID } from 'node:crypto'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { BASE_URL, createAppVersions, createDirectApiKeyWithBindings, fetchBundle, getSupabaseClient, headers, ORG_ID, resetAndSeedAppData, resetAppData, resetAppDataStats, USER_ID } from './test-utils.ts'

const id = randomUUID()
const APPNAME = `com.app.b.${id}`
const RBAC_APPNAME = `com.app.b.rbac.${id}`
const RBAC_ORG_ID = randomUUID()

async function putBundleToChannel(body: { app_id: string, version_id: number, channel_id: number }): Promise<Response> {
  return fetch(`${BASE_URL}/bundle`, {
    method: 'PUT',
    headers,
    body: JSON.stringify(body),
  })
}

beforeAll(async () => {
  await resetAndSeedAppData(APPNAME)
})
afterAll(async () => {
  await resetAppData(APPNAME)
  await resetAppDataStats(APPNAME)
})

describe('[GET] /bundle operations', () => {
  it('valid app_id', async () => {
    const { response, data } = await fetchBundle(APPNAME)
    expect(response.status).toBe(200)
    expect(Array.isArray(data)).toBe(true)
  })

  it('invalid app_id', async () => {
    const { response } = await fetchBundle('invalid_app')
    expect(response.status).toBe(400)
  })
})

describe('[POST] /bundle/metadata operations', () => {
  let versionId: number

  beforeAll(async () => {
    // Create a test version to update
    const version = await createAppVersions('1.0.0-test-metadata', APPNAME)
    versionId = version.id
  })

  it('should update bundle metadata successfully', async () => {
    const response = await fetch(`${BASE_URL}/bundle/metadata`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        app_id: APPNAME,
        version_id: versionId,
        link: 'https://example.com/docs',
        comment: 'Test bundle comment',
      }),
    })

    const data = await response.json() as { status: string }
    expect(response.status).toBe(200)
    expect(data.status).toBe('ok')

    // Verify the data was updated in the database
    const supabase = getSupabaseClient()
    const { data: version, error } = await supabase
      .from('app_versions')
      .select('*')
      .eq('id', versionId)
      .eq('app_id', APPNAME)
      .single()

    expect(error).toBeNull()
    expect(version).toBeTruthy()
    // Type assertion to access the new fields
    expect((version as any).link).toBe('https://example.com/docs')
    expect((version as any).comment).toBe('Test bundle comment')
  })

  it('should handle missing required fields', async () => {
    const response = await fetch(`${BASE_URL}/bundle/metadata`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        // Missing app_id
        version_id: versionId,
        link: 'https://example.com',
      }),
    })

    expect(response.status).toBe(400)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('missing_required_fields')
  })

  it('should handle invalid version_id', async () => {
    const response = await fetch(`${BASE_URL}/bundle/metadata`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        app_id: APPNAME,
        version_id: 999999, // Non-existent version ID
        link: 'https://example.com',
      }),
    })

    expect(response.status).toBe(400)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('cannot_find_version')
  })

  it('should handle invalid app_id', async () => {
    const response = await fetch(`${BASE_URL}/bundle/metadata`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        app_id: 'invalid_app',
        version_id: versionId,
        link: 'https://example.com',
      }),
    })

    expect(response.status).toBe(400)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('invalid_app_id') // Changed: validation now catches invalid format first
  })
})

describe('[DELETE] /bundle operations', () => {
  it('invalid version', async () => {
    const response = await fetch(`${BASE_URL}/bundle`, {
      method: 'DELETE',
      headers,
      body: JSON.stringify({
        app_id: APPNAME,
        version: 'invalid_version',
      }),
    })
    await response.arrayBuffer()
    expect(response.status).toBe(400)
  })

  it('invalid app_id', async () => {
    const response = await fetch(`${BASE_URL}/bundle`, {
      method: 'DELETE',
      headers,
      body: JSON.stringify({
        app_id: 'invalid_app',
      }),
    })
    await response.arrayBuffer()
    expect(response.status).toBe(400)
  })

  it('delete specific bundle', async () => {
    const deleteBundle = await fetch(`${BASE_URL}/bundle`, {
      method: 'DELETE',
      headers,
      body: JSON.stringify({
        app_id: APPNAME,
        version: '1.0.1',
      }),
    })
    const deleteBundleData = await deleteBundle.json() as { status: string }
    expect(deleteBundle.status).toBe(200)
    expect(deleteBundleData.status).toBe('ok')
  })

  it('does not delete a rollout target bundle linked to a channel', async () => {
    const supabase = getSupabaseClient()
    const stableVersion = await createAppVersions(`1.0.0-delete-rollout-stable-${id}`, APPNAME)
    const rolloutVersion = await createAppVersions(`1.0.0-delete-rollout-target-${id}`, APPNAME)
    const channelName = `delete-rollout-${id}`

    const { error: channelError } = await supabase
      .from('channels')
      .insert({
        app_id: APPNAME,
        name: channelName,
        version: stableVersion.id,
        rollout_version: rolloutVersion.id,
        rollout_enabled: true,
        rollout_percentage_bps: 1000,
        owner_org: ORG_ID,
        created_by: USER_ID,
      })

    expect(channelError).toBeNull()

    const deleteBundle = await fetch(`${BASE_URL}/bundle`, {
      method: 'DELETE',
      headers,
      body: JSON.stringify({
        app_id: APPNAME,
        version: rolloutVersion.name,
      }),
    })
    const deleteBundleData = await deleteBundle.json() as { error?: string }
    expect(deleteBundle.status).toBe(400)
    expect(deleteBundleData.error).toBe('cannot_delete_linked_version')

    const { data: versionAfterDelete, error: versionError } = await supabase
      .from('app_versions')
      .select('deleted')
      .eq('id', rolloutVersion.id)
      .single()

    expect(versionError).toBeNull()
    expect(versionAfterDelete?.deleted).toBe(false)
  })

  it('delete all bundles for an app', async () => {
    const deleteAllBundles = await fetch(`${BASE_URL}/bundle`, {
      method: 'DELETE',
      headers,
      body: JSON.stringify({
        app_id: APPNAME,
      }),
    })

    const deleteAllBundlesData = await deleteAllBundles.json() as { status: string }
    expect(deleteAllBundles.status).toBe(200)
    expect(deleteAllBundlesData.status).toBe('ok')
  })
})

describe('[PUT] /bundle operations - Set bundle to channel', () => {
  let versionId: number
  let channelId: number
  let writeScopedKeyId: number | undefined
  let writeScopedHeaders: Record<string, string> | undefined

  beforeAll(async () => {
    // Create a test version
    const version = await createAppVersions('1.0.0-test-channel', APPNAME)
    versionId = version.id

    const supabase = getSupabaseClient()

    // Create a test channel using proper seeded values
    const { data: channel, error } = await supabase
      .from('channels')
      .insert({
        name: 'test-channel',
        app_id: APPNAME,
        version: null,
        created_by: '6aa76066-55ef-4238-ade6-0b32334a4097', // test@capgo.app user from seed
        owner_org: '046a36ac-e03c-4590-9257-bd6c9dba9ee8', // Demo org from seed
      })
      .select()
      .single()

    if (error) {
      throw new Error(`Failed to create test channel: ${error.message}`)
    }
    if (!channel) {
      throw new Error('Failed to create test channel: channel is null')
    }
    channelId = channel.id

    const createKeyData = await createDirectApiKeyWithBindings({
      key: randomUUID(),
      name: `bundle-write-key-${APPNAME}`,
      orgId: ORG_ID,
      roleName: 'org_member',
      appId: APPNAME,
      appRoleName: 'app_developer',
    })
    if (!createKeyData.key)
      throw new Error('Failed to create write-scoped bundle key')

    writeScopedKeyId = createKeyData.id
    writeScopedHeaders = {
      'Content-Type': 'application/json',
      'capgkey': createKeyData.key,
    }
  })

  afterAll(async () => {
    if (writeScopedKeyId != null) {
      await getSupabaseClient().from('apikeys').delete().eq('id', writeScopedKeyId)
    }
  })

  it('should set bundle to channel successfully', async () => {
    const response = await putBundleToChannel({
      app_id: APPNAME,
      version_id: versionId,
      channel_id: channelId,
    })

    const data = await response.json() as { status: string, message: string }
    expect(response.status).toBe(200)
    expect(data.status).toBe('success')
    expect(data.message).toContain('Bundle')
    expect(data.message).toContain('set to channel')

    // Verify the channel was updated in the database
    const supabase = getSupabaseClient()
    const { data: channel, error } = await supabase
      .from('channels')
      .select('*')
      .eq('id', channelId)
      .eq('app_id', APPNAME)
      .single()

    expect(error).toBeNull()
    expect(channel).toBeTruthy()
    if (channel) {
      expect(channel.version).toBe(versionId)
    }
  })

  it('should reset leftover rollout when a new stable bundle is set', async () => {
    const supabase = getSupabaseClient()
    const nextVersion = await createAppVersions('1.0.1-test-channel-rollout-reset', APPNAME)
    const rolloutVersion = await createAppVersions('1.0.0-test-channel-rollout-target', APPNAME)

    const { error: leftoverError } = await supabase
      .from('channels')
      .update({
        version: versionId,
        rollout_version: rolloutVersion.id,
        rollout_enabled: true,
        rollout_percentage_bps: 10000,
      })
      .eq('id', channelId)
      .eq('app_id', APPNAME)
    expect(leftoverError).toBeNull()

    const { data: before, error: beforeError } = await supabase
      .from('channels')
      .select('version, rollout_version, rollout_enabled, rollout_id')
      .eq('id', channelId)
      .single()
    expect(beforeError).toBeNull()
    expect(before?.version).toBe(versionId)
    expect(before?.rollout_version).toBe(rolloutVersion.id)
    expect(before?.rollout_enabled).toBe(true)

    const response = await putBundleToChannel({
      app_id: APPNAME,
      version_id: nextVersion.id,
      channel_id: channelId,
    })
    expect(response.status).toBe(200)

    const { data: after, error: afterError } = await supabase
      .from('channels')
      .select('version, rollout_version, rollout_enabled, rollout_percentage_bps, rollout_id')
      .eq('id', channelId)
      .single()
    expect(afterError).toBeNull()
    expect(after?.version).toBe(nextVersion.id)
    expect(after?.rollout_version).toBeNull()
    expect(after?.rollout_enabled).toBe(false)
    expect(after?.rollout_percentage_bps).toBe(0)
    expect(after?.rollout_id).not.toBe(before?.rollout_id)
  })

  it('should keep the supported write-scoped API key bundle promotion flow working', async () => {
    if (!writeScopedHeaders) {
      throw new Error('Write-scoped bundle test key was not created')
    }

    const response = await fetch(`${BASE_URL}/bundle`, {
      method: 'PUT',
      headers: writeScopedHeaders,
      body: JSON.stringify({
        app_id: APPNAME,
        version_id: versionId,
        channel_id: channelId,
      }),
    })

    const data = await response.json() as { status: string, message: string }
    expect(response.status).toBe(200)
    expect(data.status).toBe('success')
    expect(data.message).toContain('set to channel')

    const supabase = getSupabaseClient()
    const { data: channel, error } = await supabase
      .from('channels')
      .select('version')
      .eq('id', channelId)
      .eq('app_id', APPNAME)
      .single()

    expect(error).toBeNull()
    expect(channel?.version).toBe(versionId)
  })

  it('should handle missing required fields', async () => {
    const response = await fetch(`${BASE_URL}/bundle`, {
      method: 'PUT',
      headers,
      body: JSON.stringify({
        // Missing channel_id
        app_id: APPNAME,
        version_id: versionId,
      }),
    })

    expect(response.status).toBe(400)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('missing_required_fields')
  })

  it('should handle invalid version_id', async () => {
    const response = await fetch(`${BASE_URL}/bundle`, {
      method: 'PUT',
      headers,
      body: JSON.stringify({
        app_id: APPNAME,
        version_id: 999999, // Non-existent version ID
        channel_id: channelId,
      }),
    })

    expect(response.status).toBe(400)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('cannot_find_version')
  })

  it('should handle invalid channel_id', async () => {
    const response = await fetch(`${BASE_URL}/bundle`, {
      method: 'PUT',
      headers,
      body: JSON.stringify({
        app_id: APPNAME,
        version_id: versionId,
        channel_id: 999999, // Non-existent channel ID
      }),
    })

    expect(response.status).toBe(400)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('cannot_find_channel')
  })

  it('should handle invalid app_id', async () => {
    const response = await fetch(`${BASE_URL}/bundle`, {
      method: 'PUT',
      headers,
      body: JSON.stringify({
        app_id: 'invalid_app',
        version_id: versionId,
        channel_id: channelId,
      }),
    })

    expect(response.status).toBe(400)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('invalid_app_id') // Changed: validation now catches invalid format first
  })
})

describe('[PUT] /bundle RBAC channel overrides', () => {
  let versionId: number
  let originalVersionId: number
  let channelId: number | undefined
  let rbacApiKeyId: number | undefined
  let rbacHeaders: Record<string, string> | undefined

  beforeAll(async () => {
    const supabase = getSupabaseClient()

    const { error: orgError } = await supabase.from('orgs').insert({
      id: RBAC_ORG_ID,
      name: `Bundle RBAC Org ${id}`,
      management_email: `bundle-rbac-${id}@capgo.app`,
      created_by: USER_ID,
    })
    if (orgError)
      throw orgError

    const { data: app, error: appError } = await supabase
      .from('apps')
      .insert({
        app_id: RBAC_APPNAME,
        name: `Bundle RBAC App ${id}`,
        icon_url: 'bundle-rbac-test-icon',
        owner_org: RBAC_ORG_ID,
      })
      .select('id')
      .single()
    if (appError || !app)
      throw appError ?? new Error('Failed to create RBAC bundle app')
    const appId = app.id
    if (!appId)
      throw new Error('Created RBAC bundle app is missing id')

    const { error: memberError } = await supabase.from('org_users').insert({
      org_id: RBAC_ORG_ID,
      user_id: USER_ID,
      rbac_role_name: 'org_member',
    })
    if (memberError)
      throw memberError

    await supabase
      .from('role_bindings')
      .delete()
      .eq('principal_type', 'user')
      .eq('principal_id', USER_ID)
      .eq('scope_type', 'app')
      .eq('app_id', appId)

    const { data: role, error: roleError } = await supabase
      .from('roles')
      .select('id')
      .eq('name', 'app_developer')
      .single()
    if (roleError || !role)
      throw roleError ?? new Error('Failed to find app_developer role')

    const { error: bindingError } = await supabase.from('role_bindings').insert({
      principal_type: 'user',
      principal_id: USER_ID,
      role_id: role.id,
      scope_type: 'app',
      org_id: RBAC_ORG_ID,
      app_id: appId,
      granted_by: USER_ID,
      is_direct: true,
    })
    if (bindingError)
      throw bindingError

    originalVersionId = (await createAppVersions(`1.0.0-rbac-original-${id}`, RBAC_APPNAME, {
      owner_org: RBAC_ORG_ID,
      user_id: USER_ID,
    })).id
    versionId = (await createAppVersions(`1.0.0-rbac-target-${id}`, RBAC_APPNAME, {
      owner_org: RBAC_ORG_ID,
      user_id: USER_ID,
    })).id

    const { data: channel, error: channelError } = await supabase
      .from('channels')
      .insert({
        name: `rbac-denied-channel-${id}`,
        app_id: RBAC_APPNAME,
        version: originalVersionId,
        created_by: USER_ID,
        owner_org: RBAC_ORG_ID,
      })
      .select('id')
      .single()
    if (channelError || !channel)
      throw channelError ?? new Error('Failed to create RBAC denied channel')
    channelId = channel.id

    const apiKey = await createDirectApiKeyWithBindings({
      userId: USER_ID,
      key: randomUUID(),
      name: `bundle-rbac-denied-${id}`,
      orgId: RBAC_ORG_ID,
      roleName: 'org_member',
      appId: RBAC_APPNAME,
      appRoleName: 'app_developer',
    })
    if (!apiKey.key)
      throw new Error('Failed to create RBAC API key')

    const { error: overrideError } = await supabase.from('channel_permission_overrides').insert({
      principal_type: 'apikey',
      principal_id: apiKey.rbac_id,
      channel_id: channelId,
      permission_key: 'channel.promote_bundle',
      is_allowed: false,
    })
    if (overrideError)
      throw overrideError

    rbacApiKeyId = apiKey.id
    rbacHeaders = {
      'Content-Type': 'application/json',
      'capgkey': apiKey.key,
    }
  })

  afterAll(async () => {
    const supabase = getSupabaseClient()
    if (rbacApiKeyId != null)
      await supabase.from('apikeys').delete().eq('id', rbacApiKeyId)
    if (channelId != null)
      await supabase.from('channel_permission_overrides').delete().eq('channel_id', channelId)
    await supabase.from('channels').delete().eq('app_id', RBAC_APPNAME)
    await supabase.from('app_versions').delete().eq('app_id', RBAC_APPNAME)
    await supabase.from('role_bindings').delete().eq('org_id', RBAC_ORG_ID)
    await supabase.from('org_users').delete().eq('org_id', RBAC_ORG_ID)
    await supabase.from('apps').delete().eq('app_id', RBAC_APPNAME)
    await supabase.from('orgs').delete().eq('id', RBAC_ORG_ID)
  })

  it('rejects API-key bundle promotion when the user has a target-channel deny override', async () => {
    if (channelId == null)
      throw new Error('RBAC denied channel was not created')
    if (!rbacHeaders)
      throw new Error('RBAC API key was not created')

    const response = await fetch(`${BASE_URL}/bundle`, {
      method: 'PUT',
      headers: rbacHeaders,
      body: JSON.stringify({
        app_id: RBAC_APPNAME,
        version_id: versionId,
        channel_id: channelId,
      }),
    })

    const data = await response.json() as { error: string }
    expect(response.status).toBe(400)
    expect(data.error).toBe('cannot_access_app')

    const { data: channel, error } = await getSupabaseClient()
      .from('channels')
      .select('version')
      .eq('id', channelId)
      .single()

    expect(error).toBeNull()
    expect(channel?.version).toBe(originalVersionId)
  })
})