Ctrl K
ComunidadGuías…

tests/build_time_tracking.test.ts

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

Archivo original ↗Preguntar a KiBot
import { randomUUID } from 'node:crypto'
import { createClient } from '@supabase/supabase-js'
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'
import { fetchTestRequest, getEndpointUrl, getSupabaseClient, PRODUCT_ID, resetAppData, resetAppDataStats, TEST_EMAIL, USER_ID } from './test-utils.ts'

// Generate unique IDs per test run to avoid conflicts with parallel test runs
const testRunId = randomUUID()
const ORG_ID = testRunId // Use UUID directly as org_id
const STRIPE_CUSTOMER_ID = `cus_build_time_${testRunId.slice(0, 8)}`
const APPNAME = `com.build_time.${testRunId.slice(0, 8)}`
const headers = {
  'Content-Type': 'application/json',
  'apisecret': 'testsecret',
}

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

  await supabase.from('stripe_info').delete().eq('customer_id', STRIPE_CUSTOMER_ID)
  await supabase.from('org_users').delete().eq('org_id', ORG_ID)
  await supabase.from('orgs').delete().eq('id', ORG_ID)

  const { error: stripeError } = await supabase
    .from('stripe_info')
    .insert({
      subscription_id: `sub_build_time_test_${testRunId.slice(0, 8)}`,
      customer_id: STRIPE_CUSTOMER_ID,
      status: 'succeeded' as const,
      product_id: PRODUCT_ID,
      trial_at: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(),
      is_good_plan: true,
      plan_usage: 2,
      subscription_anchor_start: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(),
      subscription_anchor_end: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(),
      mau_exceeded: false,
      storage_exceeded: false,
      bandwidth_exceeded: false,
      build_time_exceeded: false,
    })
  if (stripeError)
    throw stripeError

  const { error: orgError } = await supabase.from('orgs').insert({
    id: ORG_ID,
    customer_id: STRIPE_CUSTOMER_ID,
    name: `Build Time Test Org ${testRunId.slice(0, 8)}`,
    created_by: USER_ID,
    management_email: TEST_EMAIL,
  })
  if (orgError)
    throw orgError

  const { error: orgUserError } = await supabase.from('org_users').insert({
    org_id: ORG_ID,
    user_id: USER_ID,
    rbac_role_name: 'org_super_admin',
  })
  if (orgUserError)
    throw orgUserError
})

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

  await supabase.from('daily_build_time').delete().eq('app_id', APPNAME)
  await supabase.from('build_logs').delete().eq('org_id', ORG_ID)
  await supabase.from('app_metrics_cache').delete().eq('org_id', ORG_ID)
  await supabase.from('usage_credit_consumptions').delete().eq('org_id', ORG_ID)
  await supabase.from('usage_overage_events').delete().eq('org_id', ORG_ID)
  await supabase.from('usage_credit_transactions').delete().eq('org_id', ORG_ID)
  await supabase.from('usage_credit_grants').delete().eq('org_id', ORG_ID)
  await supabase.from('app_versions').delete().eq('app_id', APPNAME)
  await supabase.from('apps').delete().eq('app_id', APPNAME)

  // Reset stripe_info to clean state before each test
  const { error } = await supabase
    .from('stripe_info')
    .update({
      status: 'succeeded' as const,
      product_id: PRODUCT_ID,
      trial_at: new Date(0).toISOString(),
      is_good_plan: true,
      plan_usage: 2,
      subscription_anchor_start: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(),
      subscription_anchor_end: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(),
      mau_exceeded: false,
      storage_exceeded: false,
      bandwidth_exceeded: false,
      build_time_exceeded: false,
    })
    .eq('customer_id', STRIPE_CUSTOMER_ID)
  if (error)
    throw error

  // Create app for this test
  const { error: appError } = await supabase.from('apps').insert([
    {
      owner_org: ORG_ID,
      name: 'Test App Build Time',
      app_id: APPNAME,
      icon_url: 'https://example.com/icon.png',
    },
  ])
  if (appError)
    throw appError

  // Create an app_version to make the org "onboarded"
  const { error: versionError } = await supabase.from('app_versions').insert([
    {
      app_id: APPNAME,
      name: '1.0.0',
      owner_org: ORG_ID,
      user_id: USER_ID,
      storage_provider: 'r2-direct',
    },
  ])
  if (versionError)
    throw versionError
})

afterAll(async () => {
  await resetAppData(APPNAME)
  await resetAppDataStats(APPNAME)
  const supabase = getSupabaseClient()

  // Clean up all data for dedicated org
  await supabase.from('build_logs').delete().eq('org_id', ORG_ID)
  await supabase.from('daily_build_time').delete().eq('app_id', APPNAME)
  await supabase.from('app_versions').delete().eq('owner_org', ORG_ID)
  await supabase.from('apps').delete().eq('owner_org', ORG_ID)
  await supabase.from('org_users').delete().eq('org_id', ORG_ID)
  await supabase.from('orgs').delete().eq('id', ORG_ID)
  await supabase.from('stripe_info').delete().eq('customer_id', STRIPE_CUSTOMER_ID)
})

describe('build Time Tracking System', () => {
  it('should reject unauthenticated calls to record_build_time RPC', async () => {
    const supabaseUrl = process.env.SUPABASE_URL
    const supabaseAnonKey = process.env.SUPABASE_ANON_KEY
    if (!supabaseUrl || !supabaseAnonKey)
      throw new Error('SUPABASE_URL and SUPABASE_ANON_KEY are required for this test')

    const publicSupabase = createClient(supabaseUrl, supabaseAnonKey, {
      auth: {
        persistSession: false,
      },
    })

    const { error } = await publicSupabase.rpc('record_build_time', {
      p_org_id: ORG_ID,
      p_user_id: USER_ID,
      p_build_id: randomUUID(),
      p_platform: 'ios',
      p_build_time_unit: 30,
      p_app_id: APPNAME,
    })
    expect(error).toBeTruthy()
  })

  it('should reject direct REST POST to record_build_time without auth privileges', async () => {
    const supabaseUrl = process.env.SUPABASE_URL
    const supabaseAnonKey = process.env.SUPABASE_ANON_KEY
    if (!supabaseUrl || !supabaseAnonKey)
      throw new Error('SUPABASE_URL and SUPABASE_ANON_KEY are required for this test')

    const supabase = getSupabaseClient()
    const before = await supabase
      .from('build_logs')
      .select('id', { count: 'exact', head: true })
      .eq('org_id', ORG_ID)
    if (before.error)
      throw before.error
    const beforeCount = before.count ?? 0

    const buildId = `poc_buildtime_${randomUUID()}`
    const response = await fetch(new URL('/rest/v1/rpc/record_build_time', supabaseUrl), {
      method: 'POST',
      headers: {
        'apikey': supabaseAnonKey,
        'authorization': `Bearer ${supabaseAnonKey}`,
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        p_org_id: ORG_ID,
        p_user_id: USER_ID,
        p_build_id: buildId,
        p_platform: 'ios',
        p_build_time_unit: 30,
        p_app_id: APPNAME,
      }),
    })
    const after = await supabase
      .from('build_logs')
      .select('id', { count: 'exact', head: true })
      .eq('org_id', ORG_ID)
    if (after.error)
      throw after.error

    expect(response.status).not.toBe(200)
    expect(after.count).toBe(beforeCount)
  })

  it('should handle too big build time correctly', async () => {
    const supabase = getSupabaseClient()

    // Insert high build time usage directly into daily_build_time
    // Solo plan limit is 3600 seconds (60 min), so we insert way over that
    const today = new Date().toISOString().split('T')[0]
    const { error: buildTimeInsertError } = await supabase
      .from('daily_build_time')
      .insert({
        app_id: APPNAME,
        date: today,
        build_time_unit: 36000, // 10 hours in seconds (way over Solo plan limit of 3600 seconds)
        build_count: 10,
      })
    expect(buildTimeInsertError).toBeFalsy()

    const { error } = await supabase
      .from('stripe_info')
      .update({ is_good_plan: true, trial_at: new Date(0).toISOString() })
      .eq('customer_id', STRIPE_CUSTOMER_ID)
    if (error)
      throw error

    // First verify the metrics can be retrieved and show excessive build time
    const { data: totalMetrics, error: totalMetricsError } = await supabase
      .rpc('get_total_metrics', { org_id: ORG_ID })
    expect(totalMetricsError).toBeFalsy()
    console.log('Total metrics before cron:', totalMetrics)
    // Verify our inserted build time is the only one (should be exactly 36000)
    expect((totalMetrics as any)?.[0]?.build_time_unit).toBe(36000)

    const response = await fetchTestRequest(getEndpointUrl('/triggers/cron_stat_org'), {
      method: 'POST',
      headers,
      body: JSON.stringify({ orgId: ORG_ID }),
    })
    if (response.status !== 200) {
      const errorText = await response.text()
      console.error('Cron endpoint error:', response.status, errorText)
    }
    expect(response.status).toBe(200)

    const { data: stripeInfoData } = await supabase
      .from('stripe_info')
      .select('*')
      .eq('customer_id', STRIPE_CUSTOMER_ID)
      .single()
    console.log('Stripe info after cron:', stripeInfoData)
    expect(stripeInfoData?.is_good_plan).toBe(false)

    const { data: buildTimeExceeded, error: buildTimeExceededError } = await supabase
      .rpc('is_build_time_exceeded_by_org', { org_id: ORG_ID })
    expect(buildTimeExceededError).toBeFalsy()
    expect(buildTimeExceeded).toBe(true)

    // These tests verify build time exceeded is blocking build actions
    const { data: isAllowedActionBuildTime, error: isAllowedActionBuildTimeError } = await supabase
      .rpc('is_allowed_action_org_action', { orgid: ORG_ID, actions: ['build_time'] as const })
    expect(isAllowedActionBuildTimeError).toBeFalsy()
    expect(isAllowedActionBuildTime).toBe(false) // Build time should be blocked
  })

  it('should correctly reset build_time_exceeded flag directly', async () => {
    const supabase = getSupabaseClient()

    // First set high build time using build_logs
    const excessiveBuildId = `${randomUUID()}-excessive-reset`
    const { error: rpcError } = await supabase.rpc('record_build_time', {
      p_org_id: ORG_ID,
      p_user_id: USER_ID,
      p_build_id: excessiveBuildId,
      p_platform: 'ios',
      p_build_time_unit: 18000, // 5 hours, 2x multiplier = 10 hours billable
      p_app_id: APPNAME,
    })
    expect(rpcError).toBeFalsy()

    // Set build_time_exceeded to true directly (simulating what cron would do)
    const { error: setExceededError } = await supabase
      .from('stripe_info')
      .update({ build_time_exceeded: true })
      .eq('customer_id', STRIPE_CUSTOMER_ID)
    expect(setExceededError).toBeFalsy()

    // Verify build time is exceeded
    const { data: buildTimeExceededBefore, error: buildTimeExceededErrorBefore } = await supabase
      .rpc('is_build_time_exceeded_by_org', { org_id: ORG_ID })
    expect(buildTimeExceededErrorBefore).toBeFalsy()
    expect(buildTimeExceededBefore).toBe(true)

    // Reset build time by deleting build logs
    const { error: resetBuildTimeError } = await supabase
      .from('build_logs')
      .delete()
      .eq('org_id', ORG_ID)
    expect(resetBuildTimeError).toBeFalsy()

    // Reset stripe_info build_time_exceeded flag directly
    const { error: resetFlagError } = await supabase
      .from('stripe_info')
      .update({ build_time_exceeded: false })
      .eq('customer_id', STRIPE_CUSTOMER_ID)
    expect(resetFlagError).toBeFalsy()

    // Verify build time is no longer exceeded
    const { data: buildTimeExceededAfter, error: buildTimeExceededErrorAfter } = await supabase
      .rpc('is_build_time_exceeded_by_org', { org_id: ORG_ID })
    expect(buildTimeExceededErrorAfter).toBeFalsy()
    expect(buildTimeExceededAfter).toBe(false)
  })

  // Note: get_total_metrics reads from daily_build_time, which is now
  // automatically populated via a trigger on build_logs inserts/updates.

  it('should correctly record build time using RPC function (iOS 2x multiplier)', async () => {
    const supabase = getSupabaseClient()
    const buildId = randomUUID()

    const { error: rpcError } = await supabase.rpc('record_build_time', {
      p_org_id: ORG_ID,
      p_user_id: USER_ID,
      p_build_id: buildId,
      p_platform: 'ios',
      p_build_time_unit: 600, // 10 minutes
      p_app_id: APPNAME,
    })
    expect(rpcError).toBeFalsy()

    const { data: buildLog, error } = await supabase
      .from('build_logs')
      .select('*')
      .eq('build_id', buildId)
      .single()
    expect(error).toBeFalsy()
    expect(buildLog?.build_time_unit).toBe(600)
    expect(buildLog?.billable_seconds).toBe(1200) // iOS 2x multiplier
  })

  it('should correctly apply Android 1x multiplier', async () => {
    const supabase = getSupabaseClient()
    const buildId = randomUUID()

    const { error: rpcError } = await supabase.rpc('record_build_time', {
      p_org_id: ORG_ID,
      p_user_id: USER_ID,
      p_build_id: buildId,
      p_platform: 'android',
      p_build_time_unit: 150,
      p_app_id: APPNAME,
    })
    expect(rpcError).toBeFalsy()

    const { data: buildLog, error } = await supabase
      .from('build_logs')
      .select('*')
      .eq('build_id', buildId)
      .single()
    expect(error).toBeFalsy()
    expect(buildLog?.build_time_unit).toBe(150)
    expect(buildLog?.billable_seconds).toBe(150) // Android 1x multiplier
  })

  it('should upsert on duplicate build_id', async () => {
    const supabase = getSupabaseClient()
    const buildId = randomUUID()

    // First call
    await supabase.rpc('record_build_time', {
      p_org_id: ORG_ID,
      p_user_id: USER_ID,
      p_build_id: buildId,
      p_platform: 'ios',
      p_build_time_unit: 600,
      p_app_id: APPNAME,
    })

    // Second call with updated time
    const { error: rpcError } = await supabase.rpc('record_build_time', {
      p_org_id: ORG_ID,
      p_user_id: USER_ID,
      p_build_id: buildId,
      p_platform: 'ios',
      p_build_time_unit: 700,
      p_app_id: APPNAME,
    })
    expect(rpcError).toBeFalsy()

    // Should have updated, not created duplicate
    const { data: logs, error } = await supabase
      .from('build_logs')
      .select('*')
      .eq('build_id', buildId)
    expect(error).toBeFalsy()
    expect(logs?.length).toBe(1)
    expect(logs?.[0]?.build_time_unit).toBe(700)
    expect(logs?.[0]?.billable_seconds).toBe(1400)
  })

  it('should reject invalid platform', async () => {
    const supabase = getSupabaseClient()
    const buildId = randomUUID()

    const { error } = await supabase.rpc('record_build_time', {
      p_org_id: ORG_ID,
      p_user_id: USER_ID,
      p_build_id: buildId,
      p_platform: 'windows' as any,
      p_build_time_unit: 600,
      p_app_id: APPNAME,
    })
    expect(error).toBeTruthy()
  })

  it('should reject negative build time', async () => {
    const supabase = getSupabaseClient()
    const buildId = randomUUID()

    const { error } = await supabase.rpc('record_build_time', {
      p_org_id: ORG_ID,
      p_user_id: USER_ID,
      p_build_id: buildId,
      p_platform: 'ios',
      p_build_time_unit: -100,
      p_app_id: APPNAME,
    })
    expect(error).toBeTruthy()
  })
})