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:
- Unit tests - SDK initialization fails without a browser
- Integration tests - Real connections to BotSigged affect test isolation
- E2E tests - Automated tests may be flagged as bots
- 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();
});