Ctrl K
ComunidadGuías…

tests/console-images.test.ts

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

Archivo original ↗Preguntar a KiBot
import { randomUUID } from 'node:crypto'
import { afterAll, describe, expect, it } from 'vitest'
import { getAuthHeaders, getEndpointUrl, getSupabaseClient, ORG_ID, USER_ID, USER_ID_2, USER_ID_NONMEMBER } from './test-utils'

// Images live in R2 under `images/`. Locally the S3 endpoint is the Supabase
// Storage `capgo` bucket, which is where these objects must land; nothing may
// be written to the legacy Supabase `images` bucket any more.
const LOCAL_R2_BUCKET = 'capgo'
const PNG_BYTES = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 1, 2, 3, 4])

interface StorageResponse<T> {
  data: T | null
  error: { message: string, statusCode?: string } | null
}

const createdPaths: string[] = []

async function upload(path: string, options: { upsert?: boolean } = {}) {
  const response = await fetch(getEndpointUrl('/private/console/images/upload'), {
    method: 'POST',
    headers: {
      ...await getAuthHeaders(),
      'Content-Type': 'image/png',
      'x-image-path': path,
      'x-image-upsert': String(options.upsert ?? false),
    },
    body: PNG_BYTES,
  })
  expect(response.status).toBe(200)
  return response.json() as Promise<StorageResponse<{ path: string }>>
}

async function sign(path: string) {
  const response = await fetch(getEndpointUrl('/private/console/images/sign'), {
    method: 'POST',
    headers: { ...await getAuthHeaders(), 'Content-Type': 'application/json' },
    body: JSON.stringify({ path, expiresIn: 60 }),
  })
  expect(response.status).toBe(200)
  return response.json() as Promise<StorageResponse<{ signedUrl: string }>>
}

async function imageToken() {
  const response = await fetch(getEndpointUrl('/private/console/images/token'), {
    method: 'POST',
    headers: await getAuthHeaders(),
  })
  expect(response.status).toBe(200)
  const body = await response.json() as StorageResponse<{ token: string, expiresAt: number }>
  expect(body.error).toBeNull()
  return body.data!.token
}

function readUrl(path: string, token: string) {
  const url = new URL(getEndpointUrl('/private/console/images/read'))
  url.searchParams.set('path', path)
  url.searchParams.set('t', token)
  return url
}

async function remove(paths: string[]) {
  const response = await fetch(getEndpointUrl('/private/console/images/remove'), {
    method: 'POST',
    headers: { ...await getAuthHeaders(), 'Content-Type': 'application/json' },
    body: JSON.stringify({ paths }),
  })
  expect(response.status).toBe(200)
  return response.json() as Promise<StorageResponse<{ name: string }[]>>
}

async function r2Has(path: string) {
  const folder = path.split('/').slice(0, -1).join('/')
  const name = path.split('/').at(-1)
  const { data } = await getSupabaseClient().storage.from(LOCAL_R2_BUCKET).list(`images/${folder}`, { search: name })
  return data?.some(entry => entry.name === name) ?? false
}

afterAll(async () => {
  if (createdPaths.length > 0)
    await getSupabaseClient().storage.from(LOCAL_R2_BUCKET).remove(createdPaths.map(path => `images/${path}`))
})

describe('console image endpoints (R2)', () => {
  it('uploads, signs, reads and removes an avatar through R2', async () => {
    const path = `${USER_ID}/r2-avatar-${randomUUID()}.png`
    createdPaths.push(path)

    const uploaded = await upload(path)
    expect(uploaded.error).toBeNull()
    expect(uploaded.data?.path).toBe(path)
    expect(await r2Has(path)).toBe(true)
    const { data: legacy } = await getSupabaseClient().storage.from('images').list(USER_ID, { search: path.split('/').at(-1) })
    expect(legacy ?? []).toHaveLength(0)

    const conflict = await upload(path)
    expect(conflict.error?.statusCode).toBe('409')

    // One token reads every image the user may see; the URL is built locally.
    const token = await imageToken()
    const read = await fetch(readUrl(path, token))
    expect(read.status).toBe(200)
    expect(read.headers.get('content-type')).toBe('image/png')
    expect(read.headers.get('x-content-type-options')).toBe('nosniff')
    expect(new Uint8Array(await read.arrayBuffer())).toEqual(PNG_BYTES)

    const etag = read.headers.get('etag')
    expect(etag).toBeTruthy()
    const revalidated = await fetch(readUrl(path, token), { headers: { 'if-none-match': etag! } })
    expect(revalidated.status).toBe(304)

    expect((await fetch(readUrl(path, `${token}x`))).status).toBe(401)
    // The token is the user's, not the path's: another user's private folder stays closed.
    expect((await fetch(readUrl(`${USER_ID_NONMEMBER}/avatar.png`, token))).status).toBe(404)

    // The legacy sign endpoint now returns the same kind of relay URL.
    const signed = await sign(path)
    expect(signed.error).toBeNull()
    const signedUrl = new URL(signed.data!.signedUrl)
    expect(signedUrl.pathname).toMatch(/\/images\/read$/)
    expect(signedUrl.searchParams.get('t')).toBeTruthy()

    const removed = await remove([path])
    expect(removed.error).toBeNull()
    expect(removed.data).toEqual([{ name: path }])
    expect(await r2Has(path)).toBe(false)

    const missing = await sign(path)
    expect(missing.error?.statusCode).toBe('404')
  })

  it('uploads an org logo for an org the caller administers', async () => {
    const path = `org/${ORG_ID}/logo/r2-logo-${randomUUID()}.png`
    createdPaths.push(path)

    const uploaded = await upload(path, { upsert: true })
    expect(uploaded.error).toBeNull()
    expect(await r2Has(path)).toBe(true)
  })

  it('reads a teammate avatar but not an outsider avatar', async () => {
    // Seeded directly in the local R2 stand-in: USER_ID_2 shares ORG_ID with the
    // caller (org.read_members); USER_ID_NONMEMBER shares no organization.
    const teammate = `${USER_ID_2}/r2-teammate-${randomUUID()}.png`
    const outsider = `${USER_ID_NONMEMBER}/r2-outsider-${randomUUID()}.png`
    for (const path of [teammate, outsider]) {
      createdPaths.push(path)
      const { error } = await getSupabaseClient().storage.from(LOCAL_R2_BUCKET).upload(`images/${path}`, new Blob([PNG_BYTES], { type: 'image/png' }), { upsert: true })
      expect(error).toBeNull()
    }

    const token = await imageToken()
    expect((await fetch(readUrl(teammate, token))).status).toBe(200)
    expect((await fetch(readUrl(outsider, token))).status).toBe(404)
  })

  it('refuses to write or delete another user folder', async () => {
    const path = `${USER_ID_NONMEMBER}/r2-forbidden-${randomUUID()}.png`

    const uploaded = await upload(path, { upsert: true })
    expect(uploaded.error?.statusCode).toBe('403')
    expect(await r2Has(path)).toBe(false)

    const removed = await remove([path])
    expect(removed.data).toEqual([])
  })
})