Guides

Testing & QA

Test with BotSigged enabled, mock the SDK, and configure staging environments

Testing & QA

This guide covers testing strategies when BotSigged is integrated into your application, including mocking the SDK for unit tests, integration testing, and staging environment configuration.

Testing Challenges

When BotSigged is integrated, you’ll face these testing challenges:

  1. Unit tests - SDK initialization fails without a browser
  2. Integration tests - Real connections to BotSigged affect test isolation
  3. E2E tests - Automated tests may be flagged as bots
  4. CI/CD - No browser environment for SDK initialization

Mocking the SDK

Basic Mock

Create a mock that matches the SDK interface:

// __mocks__/botsigged.ts
export class MockBotSigged {
  private score = 25; // Default human-like score
  private connected = true;
  private sessionId = 'test-session-123';

  static instance: MockBotSigged | null = null;

  constructor(config: any) {
    MockBotSigged.instance = this;

    if (config.onConnectionChange) {
      setTimeout(() => config.onConnectionChange(true), 0);
    }

    if (config.onScoreUpdate) {
      setTimeout(() => {
        config.onScoreUpdate({
          bot_score: this.score,
          classification: 'human',
          triggered_rules: [],
        });
      }, 10);
    }
  }

  static init(config: any) {
    return new MockBotSigged(config);
  }

  static getInstance() {
    return MockBotSigged.instance;
  }

  static destroy() {
    MockBotSigged.instance = null;
  }

  async start() {
    return Promise.resolve();
  }

  async stop() {
    return Promise.resolve();
  }

  getSessionId() {
    return this.sessionId;
  }

  getLastScore() {
    return {
      bot_score: this.score,
      classification: 'human',
      triggered_rules: [],
    };
  }

  isConnected() {
    return this.connected;
  }

  async waitUntilReady() {
    return { score: this.score, timedOut: false };
  }

  canSubmit() {
    return { allowed: true, reason: null, score: this.score };
  }

  withProtection<T>(fn: () => Promise<T>) {
    return fn;
  }

  async triggerChallenge() {
    return { solved: true, timeMs: 100 };
  }

  // Test helpers
  setScore(score: number) {
    this.score = score;
  }

  setConnected(connected: boolean) {
    this.connected = connected;
  }

  simulateHighScore() {
    this.score = 85;
  }

  simulateDisconnect() {
    this.connected = false;
  }
}

export const BotSigged = MockBotSigged;

Jest Configuration

// jest.config.js
module.exports = {
  moduleNameMapper: {
    '@botsigged/sdk': '<rootDir>/__mocks__/botsigged.ts',
  },
};

Vitest Configuration

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    alias: {
      '@botsigged/sdk': './__mocks__/botsigged.ts',
    },
  },
});

Unit Testing

Testing Protected Components

// SignupForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { SignupForm } from './SignupForm';
import { MockBotSigged } from '@botsigged/sdk';

describe('SignupForm', () => {
  beforeEach(() => {
    MockBotSigged.destroy();
  });

  it('submits form when bot score is low', async () => {
    const onSubmit = jest.fn();
    render(<SignupForm onSubmit={onSubmit} />);

    fireEvent.change(screen.getByLabelText('Email'), {
      target: { value: '[email protected]' },
    });
    fireEvent.click(screen.getByText('Sign Up'));

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalled();
    });
  });

  it('blocks submission when bot score is high', async () => {
    // Simulate high bot score
    MockBotSigged.getInstance()?.simulateHighScore();

    const onSubmit = jest.fn();
    render(<SignupForm onSubmit={onSubmit} />);

    fireEvent.change(screen.getByLabelText('Email'), {
      target: { value: '[email protected]' },
    });
    fireEvent.click(screen.getByText('Sign Up'));

    await waitFor(() => {
      expect(screen.getByText(/unable to verify/i)).toBeInTheDocument();
    });

    expect(onSubmit).not.toHaveBeenCalled();
  });

  it('shows loading state while verifying', async () => {
    render(<SignupForm />);

    // Before SDK ready
    expect(screen.getByText('Verifying...')).toBeInTheDocument();

    // After SDK ready
    await waitFor(() => {
      expect(screen.getByText('Sign Up')).toBeInTheDocument();
    });
  });
});

Testing Custom Hooks

// useBotProtection.test.ts
import { renderHook, act } from '@testing-library/react-hooks';
import { useProtectedSubmit } from './useProtectedSubmit';
import { MockBotSigged } from '@botsigged/sdk';

describe('useProtectedSubmit', () => {
  it('allows submission with low score', async () => {
    const submitFn = jest.fn().mockResolvedValue({ success: true });

    const { result } = renderHook(() => useProtectedSubmit(submitFn));

    await act(async () => {
      await result.current.submit();
    });

    expect(submitFn).toHaveBeenCalled();
    expect(result.current.error).toBeNull();
  });

  it('blocks submission with high score', async () => {
    MockBotSigged.getInstance()?.setScore(85);
    const submitFn = jest.fn();

    const { result } = renderHook(() =>
      useProtectedSubmit(submitFn, { threshold: 70 })
    );

    await act(async () => {
      await result.current.submit();
    });

    expect(submitFn).not.toHaveBeenCalled();
    expect(result.current.error).toBe('Request blocked due to suspicious activity');
  });
});

Testing Vue Components

// SignupForm.spec.ts
import { mount } from '@vue/test-utils';
import { MockBotSigged } from '@botsigged/sdk';
import SignupForm from './SignupForm.vue';

describe('SignupForm', () => {
  it('submits when score is low', async () => {
    const wrapper = mount(SignupForm);

    await wrapper.find('input[type="email"]').setValue('[email protected]');
    await wrapper.find('form').trigger('submit');

    // Wait for async operations
    await wrapper.vm.$nextTick();

    expect(wrapper.emitted('submit')).toBeTruthy();
  });

  it('shows error when score is high', async () => {
    MockBotSigged.getInstance()?.setScore(85);

    const wrapper = mount(SignupForm);

    await wrapper.find('form').trigger('submit');
    await wrapper.vm.$nextTick();

    expect(wrapper.find('.error').text()).toContain('unable to verify');
  });
});

Integration Testing

Testing with Real SDK (Sandbox Mode)

BotSigged provides a sandbox API key for testing:

const botSigged = BotSigged.init({
  apiKey: 'sandbox-test-key', // Always returns predictable scores
  endpoint: 'wss://sandbox.botsigged.com/socket',
});

Sandbox behavior:

  • Email containing bot@ returns score 90
  • Email containing human@ returns score 10
  • All other requests return score 50

Cypress E2E Tests

// cypress/support/commands.js
Cypress.Commands.add('mockBotSigged', (score = 25) => {
  cy.intercept('GET', '**/socket/**', { statusCode: 101 }).as('websocket');

  cy.window().then((win) => {
    win.BotSigged = {
      init: () => ({
        getSessionId: () => 'cypress-test-session',
        getLastScore: () => ({
          bot_score: score,
          classification: score > 70 ? 'stealth_bot' : 'human',
        }),
        waitUntilReady: () => Promise.resolve({ score, timedOut: false }),
        canSubmit: () => ({ allowed: score < 70, score }),
        isConnected: () => true,
      }),
    };
  });
});
// cypress/e2e/checkout.cy.js
describe('Checkout', () => {
  beforeEach(() => {
    cy.mockBotSigged(25);
  });

  it('completes checkout for human users', () => {
    cy.visit('/checkout');
    cy.get('[data-testid="email"]').type('[email protected]');
    cy.get('[data-testid="submit"]').click();
    cy.url().should('include', '/confirmation');
  });

  it('blocks checkout for suspected bots', () => {
    cy.mockBotSigged(85);
    cy.visit('/checkout');
    cy.get('[data-testid="email"]').type('[email protected]');
    cy.get('[data-testid="submit"]').click();
    cy.get('[data-testid="error"]').should('contain', 'unable to verify');
  });
});

Playwright E2E Tests

// tests/checkout.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Checkout', () => {
  test.beforeEach(async ({ page }) => {
    // Inject mock before page loads
    await page.addInitScript(() => {
      window.__BOTSIGGED_MOCK__ = {
        score: 25,
        classification: 'human',
      };
    });
  });

  test('completes checkout', async ({ page }) => {
    await page.goto('/checkout');
    await page.fill('[data-testid="email"]', '[email protected]');
    await page.click('[data-testid="submit"]');
    await expect(page).toHaveURL(/confirmation/);
  });

  test('blocks bots', async ({ page }) => {
    await page.addInitScript(() => {
      window.__BOTSIGGED_MOCK__ = {
        score: 85,
        classification: 'stealth_bot',
      };
    });

    await page.goto('/checkout');
    await page.click('[data-testid="submit"]');
    await expect(page.locator('[data-testid="error"]')).toBeVisible();
  });
});

Application code to support mocking:

// Check for test mock
const mockConfig = window.__BOTSIGGED_MOCK__;

const botSigged = mockConfig
  ? createMockBotSigged(mockConfig)
  : BotSigged.init({ apiKey: 'your-api-key' });

Staging Environment

Environment-Based Configuration

const config = {
  development: {
    apiKey: 'dev-api-key',
    debug: true,
    actionThreshold: 90, // Very permissive for dev
  },
  staging: {
    apiKey: 'staging-api-key',
    debug: true,
    actionThreshold: 70,
  },
  production: {
    apiKey: process.env.BOTSIGGED_API_KEY,
    debug: false,
    actionThreshold: 60,
  },
};

const env = process.env.NODE_ENV || 'development';
BotSigged.init(config[env]);

Bypass for Internal Testing

Allow testers to bypass protection:

// Check for testing cookie/header
const isInternalTest =
  document.cookie.includes('botsigged_bypass=true') ||
  new URLSearchParams(location.search).has('__botsigged_test');

const botSigged = BotSigged.init({
  apiKey: 'your-api-key',
  // Disable actions for internal testing
  action: isInternalTest ? 'none' : 'challenge',
  actionThreshold: isInternalTest ? 100 : 60,
});

QA Dashboard Access

Provide QA team with score visibility:

// Only in non-production
if (process.env.NODE_ENV !== 'production') {
  // Add debug panel
  const panel = document.createElement('div');
  panel.id = 'botsigged-debug';
  panel.style.cssText = 'position:fixed;bottom:10px;right:10px;background:#333;color:#fff;padding:10px;z-index:9999;font-family:monospace;font-size:12px;';
  document.body.appendChild(panel);

  BotSigged.init({
    apiKey: 'your-api-key',
    debug: true,
    onScoreUpdate: (score) => {
      panel.innerHTML = `
        Score: ${score.bot_score}<br>
        Class: ${score.classification}<br>
        Rules: ${score.triggered_rules.join(', ') || 'none'}
      `;
    },
  });
}

CI/CD Configuration

Skip SDK in CI

// Detect CI environment
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';

if (!isCI) {
  BotSigged.init({ apiKey: 'your-api-key' });
}

GitHub Actions Example

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    env:
      CI: true
      BOTSIGGED_ENABLED: false

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci
      - run: npm test

Conditional Initialization

// lib/botsigged.ts
export function initBotSigged() {
  // Skip in test environments
  if (
    typeof window === 'undefined' ||
    process.env.NODE_ENV === 'test' ||
    process.env.CI === 'true'
  ) {
    return createMockBotSigged();
  }

  return BotSigged.init({
    apiKey: process.env.NEXT_PUBLIC_BOTSIGGED_KEY!,
  });
}

Test Utilities

Score Simulator

Helper for testing different score scenarios:

// test-utils/botsigged.ts
export function createScoreSimulator() {
  let currentScore = 25;
  const listeners: ((score: number) => void)[] = [];

  return {
    setScore(score: number) {
      currentScore = score;
      listeners.forEach((fn) => fn(score));
    },

    onScoreChange(fn: (score: number) => void) {
      listeners.push(fn);
      return () => {
        const idx = listeners.indexOf(fn);
        if (idx > -1) listeners.splice(idx, 1);
      };
    },

    getScore() {
      return currentScore;
    },

    // Presets
    simulateHuman() {
      this.setScore(15);
    },
    simulateSuspicious() {
      this.setScore(55);
    },
    simulateBot() {
      this.setScore(90);
    },
  };
}

Test Wrapper Component

// test-utils/BotSiggedTestProvider.tsx
import { createContext, useContext, useState } from 'react';

const TestContext = createContext({
  score: 25,
  setScore: (n: number) => {},
});

export function BotSiggedTestProvider({
  children,
  initialScore = 25,
}) {
  const [score, setScore] = useState(initialScore);

  const mockSdk = {
    getLastScore: () => ({
      bot_score: score,
      classification: score > 70 ? 'stealth_bot' : score > 40 ? 'suspicious' : 'human',
    }),
    waitUntilReady: () => Promise.resolve({ score, timedOut: false }),
    canSubmit: () => ({ allowed: score < 70, score }),
    getSessionId: () => 'test-session',
    isConnected: () => true,
  };

  return (
    <TestContext.Provider value={{ score, setScore }}>
      <BotSiggedContext.Provider value={{ sdk: mockSdk, score: mockSdk.getLastScore(), isReady: true }}>
        {children}
      </BotSiggedContext.Provider>
    </TestContext.Provider>
  );
}

// Helper hook for tests
export function useTestControls() {
  return useContext(TestContext);
}

Usage in tests:

import { BotSiggedTestProvider, useTestControls } from '@/test-utils';

function TestControls() {
  const { setScore } = useTestControls();

  return (
    <div>
      <button onClick={() => setScore(10)}>Human</button>
      <button onClick={() => setScore(50)}>Suspicious</button>
      <button onClick={() => setScore(90)}>Bot</button>
    </div>
  );
}

test('handles score changes', async () => {
  render(
    <BotSiggedTestProvider>
      <TestControls />
      <ProtectedForm />
    </BotSiggedTestProvider>
  );

  // Start as human
  expect(screen.getByText('Submit')).toBeEnabled();

  // Change to bot
  fireEvent.click(screen.getByText('Bot'));
  expect(screen.getByText('Submit')).toBeDisabled();
});

Debugging Test Failures

Enable Debug Logging

BotSigged.init({
  apiKey: 'your-api-key',
  debug: true, // Logs all SDK activity
});

Check Mock State

// In your test
console.log('Mock state:', {
  score: MockBotSigged.getInstance()?.getLastScore(),
  connected: MockBotSigged.getInstance()?.isConnected(),
  sessionId: MockBotSigged.getInstance()?.getSessionId(),
});

Verify SDK Initialization

test('SDK initializes correctly', async () => {
  const { result } = renderHook(() => useBotSigged());

  // Wait for initialization
  await waitFor(() => {
    expect(result.current.isReady).toBe(true);
  });

  expect(result.current.sdk).toBeDefined();
  expect(result.current.score).not.toBeNull();
});