Testing Guide
Complete guide for testing your Loyalteez integration.
🎯 Test Before You Ship: Ensure your integration works perfectly before going live.
Testing Strategy
1. Unit Tests
Test individual functions and API calls
2. Integration Tests
Test complete user flows end-to-end
3. Manual Testing
Test in development environment
4. Production Smoke Tests
Verify production deployment
Test Environments
| Environment | Purpose | Brand ID |
|---|---|---|
| Development | Local testing | Use test Brand ID |
| Staging | Pre-production testing | Use staging Brand ID |
| Production | Live users | Production Brand ID |
Testing Event Tracking
Unit Test Example (Jest)
const { trackEvent } = require('./loyalteez');
// Mock fetch
global.fetch = jest.fn();
describe('Event Tracking', () => {
beforeEach(() => {
fetch.mockClear();
});
test('should track account_creation event', async () => {
const mockResponse = {
success: true,
eventId: '123',
rewardAmount: 100,
};
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});
const result = await trackEvent(
'account_creation',
'[email protected]'
);
expect(fetch).toHaveBeenCalledWith(
'https://api.loyalteez.app/loyalteez-api/manual-event',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
);
expect(result.success).toBe(true);
expect(result.rewardAmount).toBe(100);
});
test('should handle rate limit errors', async () => {
fetch.mockResolvedValueOnce({
ok: false,
status: 429,
json: async () => ({
error: 'Rate limit exceeded',
}),
});
await expect(
trackEvent('account_creation', '[email protected]')
).rejects.toThrow();
});
test('should handle automation disabled', async () => {
fetch.mockResolvedValueOnce({
ok: false,
status: 403,
json: async () => ({
error: 'Automation disabled',
}),
});
await expect(
trackEvent('account_creation', '[email protected]')
).rejects.toThrow();
});
});
Testing Gasless Transactions
Integration Test Example
import { renderHook, act, waitFor } from '@testing-library/react';
import { useLoyalteez } from './useLoyalteez';
// Mock Privy
jest.mock('@privy-io/react-auth', () => ({
usePrivy: () => ({
user: { wallet: { address: '0x123...' } },
getAccessToken: jest.fn().mockResolvedValue('mock_token'),
authenticated: true,
}),
}));
describe('Gasless Transactions', () => {
test('should execute gasless transaction', async () => {
const { result } = renderHook(() => useLoyalteez());
// Mock gas relayer response
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({
success: true,
transactionHash: '0xabc...',
}),
});
let txResult;
await act(async () => {
txResult = await result.current.executeTransaction({
to: '0xContract...',
data: '0x...',
gasLimit: 300000,
});
});
expect(txResult.success).toBe(true);
expect(txResult.transactionHash).toBeTruthy();
});
test('should handle invalid Privy token', async () => {
const { result } = renderHook(() => useLoyalteez());
global.fetch = jest.fn().mockResolvedValueOnce({
ok: false,
status: 401,
json: async () => ({
error: 'Invalid or expired Privy token',
}),
});
await expect(
result.current.executeTransaction({
to: '0xContract...',
data: '0x...',
})
).rejects.toThrow();
});
});
Manual Testing Checklist
Event Tracking
Test Account Creation:
# Test with cURL
curl -X POST https://api.loyalteez.app/loyalteez-api/manual-event \
-H "Content-Type: application/json" \
-d '{
"brandId": "YOUR_BRAND_ID",
"eventType": "account_creation",
"userEmail": "[email protected]"
}'
Expected Response:
{
"success": true,
"eventId": "...",
"rewardAmount": 100,
"walletCreated": true,
"walletAddress": "0x..."
}
Verify:
- Response status is 200
-
successistrue -
rewardAmountmatches expected (100 for account_creation) -
walletAddressis returned - User can see LTZ balance in wallet
Newsletter Subscribe
curl -X POST https://api.loyalteez.app/loyalteez-api/manual-event \
-H "Content-Type: application/json" \
-d '{
"brandId": "YOUR_BRAND_ID",
"eventType": "newsletter_subscribe",
"userEmail": "[email protected]"
}'
Expected Response:
{
"success": true,
"rewardAmount": 25,
...
}
Rate Limiting
Test with same email twice:
# First call - should succeed
curl -X POST https://api.loyalteez.app/loyalteez-api/manual-event \
-H "Content-Type: application/json" \
-d '{
"brandId": "YOUR_BRAND_ID",
"eventType": "newsletter_subscribe",
"userEmail": "[email protected]"
}'
# Second call immediately - should be rate limited
curl -X POST https://api.loyalteez.app/loyalteez-api/manual-event \
-H "Content-Type: application/json" \
-d '{
"brandId": "YOUR_BRAND_ID",
"eventType": "newsletter_subscribe",
"userEmail": "[email protected]"
}'
Expected Second Response:
{
"error": "Rate limit exceeded",
"message": "Too many events from this email..."
}
Status Code: 429
Verify:
- First call succeeds (200)
- Second call is rate limited (429)
- Error message is clear
OAuth Pregeneration
# Test Discord wallet pregeneration
curl -X POST https://register.loyalteez.app/loyalteez-api/pregenerate-user \
-H "Content-Type: application/json" \
-d '{
"brand_id": "YOUR_BRAND_ID",
"oauth_provider": "discord",
"oauth_user_id": "REAL_DISCORD_ID",
"oauth_username": "real_username"
}'
Expected Response:
{
"success": true,
"wallet_address": "0x...",
"privy_user_id": "...",
"created_new": true,
"message": "Wallet created for real_username"
}
Verify:
- Wallet address is returned
-
created_newistrueon first call - Same call returns
created_new: false(idempotent)
Frontend Testing
React Testing Library Example
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import EventTracker from './EventTracker';
// Mock Privy
jest.mock('@privy-io/react-auth', () => ({
usePrivy: () => ({
user: { email: { address: '[email protected]' } },
authenticated: true,
}),
PrivyProvider: ({ children }) => <div>{children}</div>,
}));
describe('EventTracker Component', () => {
test('should render event type dropdown', () => {
render(<EventTracker />);
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
});
test('should track event on button click', async () => {
// Mock fetch
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({
success: true,
rewardAmount: 100,
}),
});
render(<EventTracker />);
const button = screen.getByText(/Track Event/i);
fireEvent.click(button);
await waitFor(() => {
expect(fetch).toHaveBeenCalled();
});
// Should show success message
expect(screen.getByText(/100 LTZ/i)).toBeInTheDocument();
});
test('should show error on failure', async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: false,
status: 403,
json: async () => ({
error: 'Automation disabled',
}),
});
render(<EventTracker />);
const button = screen.getByText(/Track Event/i);
fireEvent.click(button);
await waitFor(() => {
expect(screen.getByText(/Automation disabled/i)).toBeInTheDocument();
});
});
});
End-to-End Testing
Playwright Example
import { test, expect } from '@playwright/test';
test.describe('Loyalteez Integration E2E', () => {
test('should complete full user flow', async ({ page }) => {
// 1. Navigate to app
await page.goto('http://localhost:3000');
// 2. Login with Privy
await page.click('button:has-text("Log In")');
await page.fill('input[type="email"]', '[email protected]');
await page.click('button:has-text("Continue")');
// Wait for authentication
await expect(page.locator('text=Welcome')).toBeVisible();
// 3. Track an event
await page.selectOption('select', 'account_creation');
await page.click('button:has-text("Track Event")');
// 4. Verify success
await expect(page.locator('text=100 LTZ')).toBeVisible({
timeout: 10000,
});
// 5. Check wallet balance
await page.click('button:has-text("Refresh")');
await expect(page.locator('text=/LTZ Balance: \\d+/i')).toBeVisible();
// 6. Claim a perk
await page.fill('input[type="number"]', '1');
await page.click('button:has-text("Claim Perk")');
// 7. Verify transaction
await expect(page.locator('text=Perk Claimed')).toBeVisible({
timeout: 30000, // Blockchain confirmation can take time
});
});
test('should handle errors gracefully', async ({ page }) => {
await page.goto('http://localhost:3000');
// Try to track event without login
await page.click('button:has-text("Track Event")');
await expect(page.locator('text=Please log in')).toBeVisible();
});
});
Load Testing
Test API Performance
Using Apache Bench:
# Test event tracking endpoint
ab -n 1000 -c 10 -p event.json -T application/json \
https://api.loyalteez.app/loyalteez-api/manual-event
event.json:
{
"brandId": "YOUR_BRAND_ID",
"eventType": "form_submit",
"userEmail": "[email protected]"
}
Using Artillery:
artillery-test.yml:
config:
target: "https://api.loyalteez.app"
phases:
- duration: 60
arrivalRate: 10
name: "Sustained load"
scenarios:
- name: "Track Events"
flow:
- post:
url: "/loyalteez-api/manual-event"
json:
brandId: "YOUR_BRAND_ID"
eventType: "form_submit"
userEmail: "loadtest{{ $randomNumber() }}@example.com"
Run:
artillery run artillery-test.yml
Test Data
Valid Test Emails
Use unique emails for each test to avoid rate limits:
function generateTestEmail(testName) {
return `test+${testName}+${Date.now()}@example.com`;
}
// Usage
const email = generateTestEmail('signup');
// [email protected]
Event Types & Rewards
| Event Type | Reward | Test Scenario |
|---|---|---|
account_creation | 100 LTZ | User signup |
complete_survey | 75 LTZ | Survey completion |
newsletter_subscribe | 25 LTZ | Newsletter form |
rate_experience | 50 LTZ | Rating widget |
subscribe_renewal | 200 LTZ | Subscription webhook |
form_submit | 10 LTZ | Generic forms |
Debugging Tests
Enable Debug Logging
// For fetch requests
const originalFetch = global.fetch;
global.fetch = async (...args) => {
console.log('📤 Fetch Request:', args[0], args[1]?.body);
const response = await originalFetch(...args);
console.log('📥 Fetch Response:', response.status);
return response;
};
Common Test Failures
"Cannot read properties of undefined"
Cause: Privy not properly mocked
Fix: Mock usePrivy hook completely
"Request timeout"
Cause: Network issue or API down
Fix: Increase timeout, check API status
"Rate limit exceeded"
Cause: Using same test email multiple times
Fix: Generate unique emails per test
"Automation disabled"
Cause: Testing with production Brand ID that has automation off
Fix: Use test Brand ID with automation enabled
CI/CD Integration
GitHub Actions Example
.github/workflows/test.yml:
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm test
env:
LOYALTEEZ_BRAND_ID: ${{ secrets.TEST_BRAND_ID }}
LOYALTEEZ_API_URL: https://api.loyalteez.app
- name: Run E2E tests
run: npm run test:e2e
env:
PRIVY_APP_ID: ${{ secrets.PRIVY_APP_ID }}
Production Testing
Smoke Tests After Deployment
Run these immediately after deploying:
# 1. Health check
curl https://yourapp.com/health
# 2. Test event tracking
curl -X POST https://api.loyalteez.app/loyalteez-api/manual-event \
-H "Content-Type: application/json" \
-d '{
"brandId": "PRODUCTION_BRAND_ID",
"eventType": "form_submit",
"userEmail": "[email protected]"
}'
# 3. Verify response
# Should return 200 with success: true
Monitoring
Set up alerts for:
- API error rate > 5%
- Event tracking failures
- Rate limit hits
- Authentication errors
Test Checklist
Before going live, verify:
- ✅ Unit tests pass
- ✅ Integration tests pass
- ✅ E2E tests pass
- ✅ Event tracking works for all event types
- ✅ Rate limiting works correctly
- ✅ Error handling works (automation disabled, rate limits)
- ✅ Gasless transactions work
- ✅ Wallet creation works
- ✅ LTZ rewards are distributed
- ✅ Production smoke tests pass
- ✅ Monitoring is set up
Troubleshooting
Tests Failing Locally
- Check environment variables are set
- Verify Brand ID is valid
- Check automation is enabled in Partner Portal
- Use unique test emails
Tests Passing Locally But Failing in CI
- Verify secrets are configured in CI
- Check network access to Loyalteez APIs
- Increase timeouts for CI environment
Flaky Tests
- Add proper
waitForconditions - Use unique test data per run
- Mock external services
Platform-Specific Testing
Telegram Bot Testing
The Telegram bot uses Vitest with Miniflare for Cloudflare Workers testing.
Setup:
npm install --save-dev vitest @vitest/coverage-v8 miniflare
Configuration (vitest.config.js):
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'miniflare',
environmentOptions: {
bindings: {
TELEGRAM_BOT_TOKEN: 'test_bot_token_12345',
LOYALTEEZ_API_URL: 'https://api.loyalteez.app',
SHARED_SERVICES_URL: 'https://services.loyalteez.app',
SUPABASE_URL: 'https://test.supabase.co',
SUPABASE_PUBLISH_KEY: 'test_key',
SUPABASE_SECRET_KEY: 'test_key'
},
kvNamespaces: ['TELEGRAM_KV'],
},
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
});
Example Unit Test:
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { handleDaily } from '../src/commands/daily.js';
import { createMockEnv, createMockBot, createMockMessage } from './setup.js';
describe('/daily command', () => {
it('should handle daily check-in successfully', async () => {
const message = createMockMessage({ text: '/daily' });
const env = createMockEnv();
const bot = createMockBot();
// Mock group config
vi.mock('../src/services/group-config.js', () => ({
getGroupConfig: vi.fn().mockResolvedValue({
brandId: '0x123',
dailyReward: 10
})
}));
await handleDaily(message, bot, env, []);
const sentMessages = bot._getSentMessages();
expect(sentMessages).toHaveLength(1);
expect(sentMessages[0].text).toContain('Daily check-in complete');
});
});
Run Tests:
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # With coverage
See Also:
WordPress Plugin Testing
WordPress plugins can be tested using PHPUnit or PHP's built-in testing.
PHPUnit Setup:
// tests/TestShortcode.php
class TestShortcode extends WP_UnitTestCase {
public function test_balance_shortcode_renders() {
$this->factory->user->create(['user_login' => 'testuser']);
wp_set_current_user(get_user_by('login', 'testuser')->ID);
$output = do_shortcode('[loyalteez_balance]');
$this->assertStringContainsString('loyalteez-balance-widget', $output);
}
}
JavaScript Testing (for widget code):
// Use Jest or Vitest for JavaScript tests
import { render, screen } from '@testing-library/react';
import BalanceWidget from '../assets/js/balance.js';
describe('Balance Widget', () => {
it('should display balance', async () => {
global.fetch = jest.fn().mockResolvedValue({
json: async () => ({ balance: 1000 })
});
render(<BalanceWidget />);
await waitFor(() => {
expect(screen.getByText('1,000')).toBeInTheDocument();
});
});
});
REST API Testing:
# Test check-in endpoint
curl -X POST http://localhost/wp-json/loyalteez/v1/checkin \
-H "Content-Type: application/json" \
-H "X-WP-Nonce: <nonce>" \
--cookie "wordpress_logged_in_XXX=COOKIE"
See Also:
- WordPress Integration (Archived)
- WordPress Troubleshooting
Shopify App Testing
Shopify apps can be tested using Jest and Shopify's testing utilities.
Unit Test Example:
import { describe, it, expect, vi } from 'vitest';
import { handleOrderCreated } from '../src/webhooks/orders.js';
describe('Order Webhook', () => {
it('should process order and send reward event', async () => {
const mockOrder = {
id: '123',
customer: { email: '[email protected]' },
total_price: '100.00'
};
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ success: true })
});
await handleOrderCreated(mockOrder, '0x123', 'https://api.loyalteez.app');
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/manual-event'),
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('[email protected]')
})
);
});
});
Widget Testing:
import { render, screen, fireEvent } from '@testing-library/react';
import LoyalteezWidget from '../src/widget/LoyalteezWidget.tsx';
describe('Loyalteez Widget', () => {
it('should render dashboard tabs', () => {
render(<LoyalteezWidget brandId="0x123" customerEmail="[email protected]" />);
expect(screen.getByText('Balance')).toBeInTheDocument();
expect(screen.getByText('Streak')).toBeInTheDocument();
expect(screen.getByText('Perks')).toBeInTheDocument();
});
it('should handle daily check-in', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ success: true, reward: 10 })
});
render(<LoyalteezWidget brandId="0x123" customerEmail="[email protected]" />);
const checkInButton = screen.getByText('Check In Today');
fireEvent.click(checkInButton);
await waitFor(() => {
expect(screen.getByText('10 LTZ')).toBeInTheDocument();
});
});
});
See Also:
- Shopify Integration (Archived)
- Shopify Troubleshooting
Discord Bot Testing
Discord bots can be tested using Vitest with Cloudflare Workers environment.
Example Test:
import { describe, it, expect, vi } from 'vitest';
import { handleCommand } from '../src/commands/index.js';
import { createMockInteraction, createMockEnv } from './setup.js';
describe('Discord Commands', () => {
it('should handle /daily command', async () => {
const interaction = createMockInteraction({
name: 'daily',
user_id: '123456789'
});
const env = createMockEnv();
const response = await handleCommand(interaction, env, {});
expect(response.type).toBe(4); // CHANNEL_MESSAGE_WITH_SOURCE
expect(response.data.content).toContain('Daily check-in');
});
});
See Also:
Integration Testing Patterns
Testing Shared Services Integration
All platforms integrate with shared services. Test shared services integration:
describe('Shared Services Integration', () => {
it('should call streak service correctly', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
success: true,
currentStreak: 7,
multiplier: 1.5
})
});
const result = await recordActivity(env, brandId, userIdentifier, 'telegram', 'daily');
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/streak/record-activity'),
expect.objectContaining({
method: 'POST',
body: expect.stringContaining(brandId)
})
);
expect(result.currentStreak).toBe(7);
});
});
Testing Error Handling
Test error scenarios:
it('should handle API errors gracefully', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
json: async () => ({ error: 'Internal server error' })
});
await expect(recordActivity(env, brandId, userIdentifier, 'telegram', 'daily'))
.rejects.toThrow();
});
Testing Rate Limiting
Test rate limit handling:
it('should handle rate limits', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 429,
json: async () => ({ error: 'Rate limit exceeded' })
});
const result = await recordActivity(env, brandId, userIdentifier, 'telegram', 'daily');
expect(result.success).toBe(false);
expect(result.error).toContain('Rate limit');
});
Related Documentation
Need Help? Join our Discord or email [email protected]