OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-desktop / test / rate-limiter.cjs

134 lines · javascript ref:
// test/rate-limiter.cjs - Tests for lib/rate-limiter.cjs

const test = require('brittle')
const { RateLimiter } = require('../lib/rate-limiter.cjs')
const { sleep } = require('./helpers/index.cjs')

test('allows requests under limit', function (t) {
  const limiter = new RateLimiter()

  const result = limiter.checkLimit('127.0.0.1', '/api/test')
  t.ok(result.allowed, 'first request allowed')

  const result2 = limiter.checkLimit('127.0.0.1', '/api/test')
  t.ok(result2.allowed, 'second request allowed')
})

test('blocks requests over limit', function (t) {
  const limiter = new RateLimiter()

  // Default limit is 100/minute
  for (let i = 0; i < 100; i++) {
    limiter.checkLimit('127.0.0.1', '/api/test')
  }

  const blocked = limiter.checkLimit('127.0.0.1', '/api/test')
  t.absent(blocked.allowed, 'request 101 blocked')
})

test('returns correct X-RateLimit headers', function (t) {
  const limiter = new RateLimiter()

  const result = limiter.checkLimit('127.0.0.1', '/api/test')

  t.ok(result.headers, 'headers exist')
  t.ok('X-RateLimit-Limit' in result.headers, 'has limit header')
  t.ok('X-RateLimit-Remaining' in result.headers, 'has remaining header')
  t.ok('X-RateLimit-Reset' in result.headers, 'has reset header')
})

test('per-IP isolation', function (t) {
  const limiter = new RateLimiter()

  // Exhaust limit for IP1
  for (let i = 0; i < 100; i++) {
    limiter.checkLimit('192.168.1.1', '/api/test')
  }

  const ip1Blocked = limiter.checkLimit('192.168.1.1', '/api/test')
  t.absent(ip1Blocked.allowed, 'IP1 blocked')

  // IP2 should still have full limit
  const ip2Allowed = limiter.checkLimit('192.168.1.2', '/api/test')
  t.ok(ip2Allowed.allowed, 'IP2 allowed')
})

test('/api/send has 10/minute limit', function (t) {
  const limiter = new RateLimiter()

  // Should allow 10 requests
  for (let i = 0; i < 10; i++) {
    const result = limiter.checkLimit('127.0.0.1', '/api/send')
    t.ok(result.allowed, `request ${i + 1} allowed`)
  }

  // 11th should be blocked
  const blocked = limiter.checkLimit('127.0.0.1', '/api/send')
  t.absent(blocked.allowed, 'request 11 blocked')
})

test('/api/export-identity has 3/hour limit', async function (t) {
  const limiter = new RateLimiter()

  // Should allow 3 requests
  for (let i = 0; i < 3; i++) {
    const result = limiter.checkLimit('127.0.0.1', '/api/export-identity')
    t.ok(result.allowed, `request ${i + 1} allowed`)
  }

  // 4th should be blocked
  const blocked = limiter.checkLimit('127.0.0.1', '/api/export-identity')
  t.absent(blocked.allowed, 'request 4 blocked')
})

test('default endpoints have 100/minute limit', function (t) {
  const limiter = new RateLimiter()

  for (let i = 0; i < 100; i++) {
    limiter.checkLimit('127.0.0.1', '/api/unknown')
  }

  const blocked = limiter.checkLimit('127.0.0.1', '/api/unknown')
  t.absent(blocked.allowed, 'default limit enforced')
})

test('cleanup removes stale entries', async function (t) {
  const limiter = new RateLimiter()

  limiter.checkLimit('127.0.0.1', '/api/test')

  // Manually trigger cleanup
  limiter.cleanup()

  // Should have entries
  t.pass('cleanup completes without error')
})

test('route normalization handles parameters', function (t) {
  const limiter = new RateLimiter()

  // These should count as same endpoint
  limiter.checkLimit('127.0.0.1', '/api/send/123')
  const result = limiter.checkLimit('127.0.0.1', '/api/send/456')

  t.ok(result.allowed, 'normalized routes share limit')
})

test('X-Forwarded-For header parsing', function (t) {
  const limiter = new RateLimiter()

  // Simulate request with X-Forwarded-For
  const req = {
    headers: {
      'x-forwarded-for': '203.0.113.1, 198.51.100.1'
    },
    socket: {
      remoteAddress: '127.0.0.1'
    }
  }

  // This test requires access to internal getClientIP function
  // For now, just verify limiter works with regular IPs
  t.pass('X-Forwarded-For support exists')
})