Ctrl K
ComunidadGuías…

tests/app-error-cases.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, createDirectApiKeyWithBindings, fetchTestRequest, getAuthHeaders, getSupabaseClient, headers, NON_ACCESS_APP_NAME, resetAndSeedAppData, resetAppData, USER_EMAIL, USER_ID, USER_ID_2, warmEdgeEndpoint } from './test-utils.ts'

const id = randomUUID().replace(/-/g, '').slice(0, 12)
const APPNAME = `com.app.error.${id}`
const DELETE_APP_ID = `${APPNAME}.delete`
const NOT_FOUND_APP_ID = `${APPNAME}.notfound`
const PUT_APP_ID = `${APPNAME}.put`
const CREATOR_APP_ID = `${APPNAME}.creator`
const testOrgId = randomUUID()
const testStripeCustomerId = `cus_app_error_${id}`
let testApiKeyId: number | null = null
let testHeaders: Record<string, string>
let authHeaders: Record<string, string>

beforeAll(async () => {
  authHeaders = await getAuthHeaders()

  await resetAndSeedAppData(APPNAME, {
    orgId: testOrgId,
    userId: USER_ID,
    stripeCustomerId: testStripeCustomerId,
  })

  const createKey = await createDirectApiKeyWithBindings({
    userId: USER_ID,
    key: `app-error-cases-${id}-${randomUUID()}`,
    name: `app-error-cases-${id}`,
    orgId: testOrgId,
    roleName: 'org_super_admin',
  })

  if (!createKey?.id || !createKey.key) {
    throw new Error('Failed to create isolated API key for app error case tests')
  }

  testApiKeyId = createKey.id
  testHeaders = {
    'Content-Type': 'application/json',
    'Authorization': createKey.key,
  }

  await warmEdgeEndpoint(`${BASE_URL}/app`, {
    method: 'POST',
    headers: testHeaders,
    body: JSON.stringify({}),
  })
}, 60000)

afterAll(async () => {
  await resetAppData(APPNAME)
  // Clean up any test apps created during tests
  await getSupabaseClient().from('apps').delete().eq('app_id', DELETE_APP_ID)
  await getSupabaseClient().from('apps').delete().eq('app_id', PUT_APP_ID)
  await getSupabaseClient().from('apps').delete().eq('app_id', CREATOR_APP_ID)
  await getSupabaseClient().from('apps').delete().eq('app_id', NOT_FOUND_APP_ID)
  if (testApiKeyId !== null) {
    await fetchTestRequest(`${BASE_URL}/apikey/${testApiKeyId}`, {
      method: 'DELETE',
      headers: authHeaders,
    })
  }
  await getSupabaseClient().from('org_users').delete().eq('org_id', testOrgId)
  await getSupabaseClient().from('orgs').delete().eq('id', testOrgId)
  await getSupabaseClient().from('stripe_info').delete().eq('customer_id', testStripeCustomerId)
}, 60000)

describe('[POST] /app - Error Cases', () => {
  it('records the authenticated app creator', async () => {
    const response = await fetchTestRequest(`${BASE_URL}/app`, {
      method: 'POST',
      headers: testHeaders,
      body: JSON.stringify({
        owner_org: testOrgId,
        app_id: CREATOR_APP_ID,
        name: 'Creator Test App',
      }),
    })

    expect(response.status).toBe(200)
    const data = await response.json() as { onboarding: { created_by_email?: string, created_by_user_id?: string } }
    expect(data.onboarding.created_by_email).toBe(USER_EMAIL)
    expect(data.onboarding.created_by_user_id).toBe(USER_ID)
  })

  it('should return 400 when name is missing', async () => {
    const response = await fetchTestRequest(`${BASE_URL}/app`, {
      method: 'POST',
      headers: testHeaders,
      body: JSON.stringify({
        owner_org: testOrgId,
        app_id: `${APPNAME}.missingname`,
        // Missing name
      }),
    })
    expect(response.status).toBe(400)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('missing_name')
  })

  it('should return 403 when organization access is denied', async () => {
    const nonExistentOrgId = randomUUID()
    const response = await fetchTestRequest(`${BASE_URL}/app`, {
      method: 'POST',
      headers: testHeaders,
      body: JSON.stringify({
        app_id: `${APPNAME}.accessdenied`,
        name: 'Test App',
        owner_org: nonExistentOrgId,
      }),
    })
    expect(response.status).toBe(403)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('cannot_access_organization')
  })

  it('should return 403 when the user is not a member of the organization', async () => {
    const { data: org, error } = await getSupabaseClient().from('orgs').insert({
      name: `Unauthorized Org ${id}`,
      created_by: USER_ID_2,
      management_email: `test-${id}@example.com`,
    }).select().single()
    expect(error).toBeNull()
    try {
      const response = await fetchTestRequest(`${BASE_URL}/app`, {
        method: 'POST',
        headers,
        body: JSON.stringify({ app_id: `${APPNAME}.nonmember`, owner_org: org!.id, name: 'Non-member App' }),
      })
      expect(response.status).toBe(403)
      const data = await response.json() as { error: string }
      expect(data.error).toBe('cannot_access_organization')
    }
    finally {
      await getSupabaseClient().from('apps').delete().eq('app_id', `${APPNAME}.nonmember`)
      await getSupabaseClient().from('orgs').delete().eq('id', org!.id)
    }
  })

  it('should return 400 when owner_org is missing', async () => {
    const response = await fetchTestRequest(`${BASE_URL}/app`, {
      method: 'POST',
      headers,
      body: JSON.stringify({ app_id: `${APPNAME}.noorg`, name: 'No Org App' }),
    })
    expect(response.status).toBe(400)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('missing_owner_org')
  })

  it('should return 409 when app creation fails due to duplicate app id', async () => {
    // Try to create another app with the same app_id
    const response2 = await fetchTestRequest(`${BASE_URL}/app`, {
      method: 'POST',
      headers: testHeaders,
      body: JSON.stringify({
        app_id: APPNAME, // Same app_id as the one created in beforeAll
        name: APPNAME,
        owner_org: testOrgId,
      }),
    })
    expect(response2.status).toBe(409)
    const data = await response2.json() as { error: string }
    expect(data.error).toBe('app_id_already_exists')
  })

  it('should return 400 with invalid JSON body', async () => {
    const response = await fetchTestRequest(`${BASE_URL}/app`, {
      method: 'POST',
      headers: testHeaders,
      body: 'invalid json',
    })
    expect(response.status).toBeGreaterThanOrEqual(400)
  })
})

describe('[GET] /app - Error Cases', () => {
  it('should return 401 when user cannot access the app', async () => {
    const response = await fetchTestRequest(`${BASE_URL}/app/nonexistent.app`, {
      method: 'GET',
      headers: testHeaders,
    })
    expect(response.status).toBe(401)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('cannot_access_app')
  })

  it('should return 401 cannot_access_app when the app was deleted', async () => {
    // Create an app first to get access, then delete it to test 404
    await resetAndSeedAppData(NOT_FOUND_APP_ID, {
      orgId: testOrgId,
      userId: USER_ID,
      stripeCustomerId: testStripeCustomerId,
    })

    // Delete the app from database directly
    await getSupabaseClient().from('apps').delete().eq('app_id', NOT_FOUND_APP_ID)

    // Try to get the deleted app
    const response = await fetchTestRequest(`${BASE_URL}/app/${NOT_FOUND_APP_ID}`, {
      method: 'GET',
      headers: testHeaders,
    })
    expect(response.status).toBe(401)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('cannot_access_app')
  })

  it('should deny an app owned by an organization the user cannot access', async () => {
    // This test would need a more complex setup with different users
    // For now, we test with a response structure check
    const response = await fetchTestRequest(`${BASE_URL}/app/${NON_ACCESS_APP_NAME}`, {
      method: 'GET',
      headers: testHeaders,
    })

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

describe('[PUT] /app - Error Cases', () => {
  beforeAll(async () => {
    // Ensure the test app exists for PUT tests
    await resetAndSeedAppData(PUT_APP_ID, {
      orgId: testOrgId,
      userId: USER_ID,
      stripeCustomerId: testStripeCustomerId,
    })
  })

  it('should return 401 when user cannot access the app', async () => {
    const response = await fetchTestRequest(`${BASE_URL}/app/nonexistent.app`, {
      method: 'PUT',
      headers: testHeaders,
      body: JSON.stringify({
        name: 'Updated Name',
      }),
    })
    expect(response.status).toBe(401)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('cannot_access_app')
  })

  it('should return cannot_update_app when the update fails', async () => {
    // Try to update with invalid data that would cause a database error
    const response = await fetchTestRequest(`${BASE_URL}/app/${PUT_APP_ID}`, {
      method: 'PUT',
      headers: testHeaders,
      body: JSON.stringify({
        // Try to set owner_org to non-existent org (this might cause an error)
        owner_org: 'non-existent-org-id',
      }),
    })

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

  it('should handle invalid JSON body', async () => {
    const response = await fetchTestRequest(`${BASE_URL}/app/${PUT_APP_ID}`, {
      method: 'PUT',
      headers: testHeaders,
      body: 'invalid json',
    })
    expect(response.status).toBeGreaterThanOrEqual(400)
  })
})

describe('[DELETE] /app - Error Cases', () => {
  it('should return 400 when user cannot access the app', async () => {
    const response = await fetchTestRequest(`${BASE_URL}/app/nonexistent.app`, {
      method: 'DELETE',
      headers: testHeaders,
    })
    expect(response.status).toBe(400)
    const data = await response.json() as { error: string }
    expect(data.error).toBe('cannot_delete_app')
  })

  it('should return 400 when app deletion fails', async () => {
    // Create an app to test deletion. Reset first so Vitest retries after a
    // previous successful delete do not reuse the seed cache for a missing row.
    await resetAppData(DELETE_APP_ID)
    await resetAndSeedAppData(DELETE_APP_ID, {
      orgId: testOrgId,
      userId: USER_ID,
      stripeCustomerId: testStripeCustomerId,
    })

    // Try to delete the app (this should work)
    const response = await fetchTestRequest(`${BASE_URL}/app/${DELETE_APP_ID}`, {
      method: 'DELETE',
      headers: testHeaders,
    })

    // The first delete should succeed
    expect(response.status).toBe(200)

    // Try to delete the same app again. Once the row is gone, the endpoint must
    // deny/fail before doing any destructive work.
    const response2 = await fetchTestRequest(`${BASE_URL}/app/${DELETE_APP_ID}`, {
      method: 'DELETE',
      headers: testHeaders,
    })
    expect([400, 401]).toContain(response2.status)
    const data = await response2.json() as { error: string }
    expect(['cannot_delete_app', 'cannot_access_app']).toContain(data.error)
  })
})