Ctrl K
ComunidadGuías…

sdk/typescript/tests/webhook.test.ts

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

Archivo original ↗Preguntar a KiBot
import { describe, it, expect } from 'vitest'
import { createHmac } from 'node:crypto'
import { verifyWebhook, ImpriWebhookSignatureError } from '../src/index.js'

// ─── Helpers ─────────────────────────────────────────────────────────────────

function sign(
  secret: string,
  timestamp: string | number,
  nonce: string,
  rawBody: string,
): string {
  const payload = `${timestamp}.${nonce}.${rawBody}`
  const hex = createHmac('sha256', secret).update(payload, 'utf8').digest('hex')
  return `sha256=${hex}`
}

const SECRET = 'whsec_test_abc123'
const NONCE = 'deadbeef1234'
const BODY = '{"event":"decision","action_id":"act_1","verdict":"approve"}'
const NOW_SEC = Math.floor(Date.now() / 1000)
const TS = String(NOW_SEC)
const VALID_SIG = sign(SECRET, TS, NONCE, BODY)

// ─── Tests ────────────────────────────────────────────────────────────────────

describe('verifyWebhook', () => {
  it('accepts a valid string body', () => {
    expect(() => verifyWebhook(BODY, SECRET, TS, NONCE, VALID_SIG)).not.toThrow()
  })

  it('accepts a Buffer body equivalent to the string', () => {
    const buf = Buffer.from(BODY, 'utf8')
    expect(() => verifyWebhook(buf, SECRET, TS, NONCE, VALID_SIG)).not.toThrow()
  })

  it('throws ImpriWebhookSignatureError for a wrong secret', () => {
    const wrongSig = sign('wrong_secret', TS, NONCE, BODY)
    expect(() => verifyWebhook(BODY, SECRET, TS, NONCE, wrongSig)).toThrow(
      ImpriWebhookSignatureError,
    )
  })

  it('throws ImpriWebhookSignatureError for a tampered body', () => {
    const tampered = BODY.replace('approve', 'reject')
    expect(() => verifyWebhook(tampered, SECRET, TS, NONCE, VALID_SIG)).toThrow(
      ImpriWebhookSignatureError,
    )
  })

  it('throws ImpriWebhookSignatureError for a tampered nonce', () => {
    const wrongNonce = 'ffffffff9999'
    expect(() => verifyWebhook(BODY, SECRET, TS, wrongNonce, VALID_SIG)).toThrow(
      ImpriWebhookSignatureError,
    )
  })

  it('throws ImpriWebhookSignatureError when signature prefix is not sha256=', () => {
    const badSig = VALID_SIG.replace('sha256=', 'md5=')
    expect(() => verifyWebhook(BODY, SECRET, TS, NONCE, badSig)).toThrow(
      ImpriWebhookSignatureError,
    )
  })

  it('throws ImpriWebhookSignatureError for an empty signature', () => {
    expect(() => verifyWebhook(BODY, SECRET, TS, NONCE, '')).toThrow(ImpriWebhookSignatureError)
  })

  it('throws ImpriWebhookSignatureError for a non-numeric timestamp', () => {
    expect(() => verifyWebhook(BODY, SECRET, 'not-a-number', NONCE, VALID_SIG)).toThrow(
      ImpriWebhookSignatureError,
    )
  })

  it('throws ImpriWebhookSignatureError for a timestamp older than toleranceSec', () => {
    const staleTs = String(NOW_SEC - 400) // 400s ago, default tolerance is 300s
    const staleSig = sign(SECRET, staleTs, NONCE, BODY)
    expect(() => verifyWebhook(BODY, SECRET, staleTs, NONCE, staleSig)).toThrow(
      ImpriWebhookSignatureError,
    )
  })

  it('accepts a timestamp within the default 300s tolerance', () => {
    const recentTs = String(NOW_SEC - 100)
    const recentSig = sign(SECRET, recentTs, NONCE, BODY)
    expect(() => verifyWebhook(BODY, SECRET, recentTs, NONCE, recentSig)).not.toThrow()
  })

  it('respects a custom toleranceSec — rejects within that window', () => {
    const mildlyStaleTs = String(NOW_SEC - 60)
    const sig = sign(SECRET, mildlyStaleTs, NONCE, BODY)
    // Tolerance of 30s should reject a 60s-old timestamp
    expect(() =>
      verifyWebhook(BODY, SECRET, mildlyStaleTs, NONCE, sig, { toleranceSec: 30 }),
    ).toThrow(ImpriWebhookSignatureError)
  })

  it('respects a custom toleranceSec — accepts within that window', () => {
    const mildlyStaleTs = String(NOW_SEC - 60)
    const sig = sign(SECRET, mildlyStaleTs, NONCE, BODY)
    // Tolerance of 120s should accept a 60s-old timestamp
    expect(() =>
      verifyWebhook(BODY, SECRET, mildlyStaleTs, NONCE, sig, { toleranceSec: 120 }),
    ).not.toThrow()
  })

  it('is case-sensitive: uppercase hex in signature fails', () => {
    const upperSig = 'sha256=' + VALID_SIG.slice(7).toUpperCase()
    // Uppercase hex differs from lowercase — this should fail (different bytes after Buffer.from hex decode)
    // Note: Buffer.from(hex, 'hex') is case-insensitive, so uppercase hex is actually valid.
    // The HMAC output is lowercase hex; comparing uppercase should work too.
    // This test verifies the library doesn't break on uppercase — both should pass.
    expect(() => verifyWebhook(BODY, SECRET, TS, NONCE, upperSig)).not.toThrow()
  })

  it('returns void (not a boolean) on success', () => {
    const result = verifyWebhook(BODY, SECRET, TS, NONCE, VALID_SIG)
    expect(result).toBeUndefined()
  })
})