Comprehensive guide to testing StateSet API integrations with practical examples and best practices
🔺 E2E Tests (Few)
↗ Complex user workflows
↗ Critical business paths
↗ Cross-system integration
🔷 Integration Tests (Some)
↗ API endpoint interactions
↗ Data transformation
↗ Error handling flows
🔲 Unit Tests (Many)
↗ Individual functions
↗ Input validation
↗ Business logic
↗ Error scenarios
// test/setup.js
import { StateSetClient } from 'stateset-node';
import dotenv from 'dotenv';
// Load test environment variables
dotenv.config({ path: '.env.test' });
// Test configuration
export const testConfig = {
apiKey: process.env.STATESET_TEST_API_KEY,
baseUrl: process.env.STATESET_TEST_BASE_URL || 'https://api.sandbox.stateset.com/v1',
timeout: 10000,
retries: 3
};
// Create test client
export const testClient = new StateSetClient({
apiKey: testConfig.apiKey,
baseUrl: testConfig.baseUrl,
timeout: testConfig.timeout
});
// Test data factory
export const createTestData = {
customer: (overrides = {}) => ({
email: `test+${Date.now()}@example.com`,
first_name: 'Test',
last_name: 'Customer',
phone: '+1-555-123-4567',
...overrides
}),
order: (customerId, overrides = {}) => ({
customer_id: customerId,
items: [
{
sku: 'TEST-ITEM-001',
quantity: 1,
price: 29.99
}
],
currency: 'USD',
...overrides
}),
address: (overrides = {}) => ({
street1: '123 Test Street',
city: 'Test City',
state: 'CA',
postal_code: '12345',
country: 'US',
...overrides
})
};
// Test helpers
export const testHelpers = {
generateUniqueEmail: () => `test+${Date.now()}+${Math.random().toString(36)}@example.com`,
waitFor: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
retry: async (fn, maxAttempts = 3, delay = 1000) => {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
await testHelpers.waitFor(delay * attempt);
}
}
},
cleanupResources: async (resources) => {
for (const resource of resources) {
try {
await resource.cleanup();
} catch (error) {
console.warn('Cleanup failed:', error.message);
}
}
}
};
# .env.test
STATESET_TEST_API_KEY=sk_test_51HqJx2eZvKYlo2CXcQhJZPiQdoJO4v_FGtbA8QTy9E4tY
STATESET_TEST_BASE_URL=https://api.sandbox.stateset.com/v1
NODE_ENV=test
LOG_LEVEL=warn
# Test database (if applicable)
TEST_DATABASE_URL=postgresql://user:pass@localhost:5432/test_db
# External service mocks
MOCK_STRIPE_ENABLED=true
MOCK_EMAIL_ENABLED=true
// test/unit/statesetClient.test.js
import { jest } from '@jest/globals';
import { StateSetClient } from 'stateset-node';
import { StateSetAPIError } from 'stateset-node/errors';
describe('StateSetClient', () => {
let client;
let mockFetch;
beforeEach(() => {
mockFetch = jest.fn();
global.fetch = mockFetch;
client = new StateSetClient({
apiKey: 'sk_test_123',
baseUrl: 'https://api.test.stateset.com/v1'
});
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('customers.create', () => {
it('should create customer successfully', async () => {
const customerData = {
email: 'test@example.com',
first_name: 'John',
last_name: 'Doe'
};
const expectedResponse = {
id: 'cust_123',
...customerData,
created_at: '2024-01-15T10:30:00Z'
};
mockFetch.mockResolvedValueOnce({
ok: true,
status: 201,
json: async () => expectedResponse
});
const result = await client.customers.create(customerData);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.test.stateset.com/v1/customers',
{
method: 'POST',
headers: {
'Authorization': 'Bearer sk_test_123',
'Content-Type': 'application/json'
},
body: JSON.stringify(customerData)
}
);
expect(result).toEqual(expectedResponse);
});
it('should handle validation errors', async () => {
const invalidCustomerData = {
email: 'invalid-email',
first_name: '',
last_name: 'Doe'
};
const errorResponse = {
error: {
code: 'VALIDATION_ERROR',
message: 'Request validation failed',
errors: [
{ field: 'email', message: 'Invalid email format' },
{ field: 'first_name', message: 'First name is required' }
]
}
};
mockFetch.mockResolvedValueOnce({
ok: false,
status: 400,
json: async () => errorResponse
});
await expect(client.customers.create(invalidCustomerData))
.rejects.toThrow(StateSetAPIError);
try {
await client.customers.create(invalidCustomerData);
} catch (error) {
expect(error.code).toBe('VALIDATION_ERROR');
expect(error.details.errors).toHaveLength(2);
expect(error.details.errors[0].field).toBe('email');
}
});
it('should handle network errors', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'));
await expect(client.customers.create({}))
.rejects.toThrow('Network error');
});
it('should handle rate limiting with retry', async () => {
const customerData = { email: 'test@example.com' };
const successResponse = { id: 'cust_123', ...customerData };
// First call returns rate limit error
mockFetch
.mockResolvedValueOnce({
ok: false,
status: 429,
headers: new Map([['Retry-After', '1']]),
json: async () => ({
error: {
code: 'RATE_LIMITED',
message: 'Rate limit exceeded'
}
})
})
// Second call succeeds
.mockResolvedValueOnce({
ok: true,
status: 201,
json: async () => successResponse
});
const result = await client.customers.create(customerData);
expect(mockFetch).toHaveBeenCalledTimes(2);
expect(result).toEqual(successResponse);
});
});
describe('orders.list', () => {
it('should list orders with filters', async () => {
const filters = {
status: 'shipped',
created_after: '2024-01-01',
limit: 10
};
const expectedResponse = {
orders: [
{ id: 'ord_1', status: 'shipped' },
{ id: 'ord_2', status: 'shipped' }
],
pagination: {
has_more: false,
total_count: 2
}
};
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => expectedResponse
});
const result = await client.orders.list(filters);
// Verify URL construction with query parameters
const expectedUrl = 'https://api.test.stateset.com/v1/orders?' +
'status=shipped&created_after=2024-01-01&limit=10';
expect(mockFetch).toHaveBeenCalledWith(
expectedUrl,
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
'Authorization': 'Bearer sk_test_123'
})
})
);
expect(result).toEqual(expectedResponse);
});
});
});
// test/unit/errorHandling.test.js
import { StateSetErrorHandler } from '../../src/utils/errorHandler';
describe('StateSetErrorHandler', () => {
describe('validation errors', () => {
it('should extract field errors correctly', () => {
const error = {
code: 'VALIDATION_ERROR',
message: 'Request validation failed',
details: {
errors: [
{ field: 'email', message: 'Invalid email format' },
{ field: 'phone', message: 'Phone number required' }
]
}
};
const result = StateSetErrorHandler.handle(error);
expect(result.type).toBe('validation');
expect(result.fieldErrors).toEqual({
email: 'Invalid email format',
phone: 'Phone number required'
});
});
});
describe('rate limiting', () => {
it('should handle rate limit with retry delay', () => {
const error = {
code: 'RATE_LIMITED',
details: { retry_after: 30 }
};
const result = StateSetErrorHandler.handle(error);
expect(result.type).toBe('rate_limit');
expect(result.retryAfter).toBe(30);
expect(result.message).toContain('30 seconds');
});
});
describe('duplicate resources', () => {
it('should suggest appropriate action for duplicate email', () => {
const error = {
code: 'DUPLICATE_EMAIL',
details: {
email: 'test@example.com',
existing_customer_id: 'cust_123'
}
};
const result = StateSetErrorHandler.handle(error);
expect(result.type).toBe('duplicate');
expect(result.suggestion).toBe('Try logging in instead');
});
});
});
// test/unit/orderProcessor.test.js
import { OrderProcessor } from '../../src/services/orderProcessor';
import { StateSetClient } from 'stateset-node';
jest.mock('stateset-node');
describe('OrderProcessor', () => {
let orderProcessor;
let mockClient;
beforeEach(() => {
mockClient = {
customers: {
create: jest.fn(),
get: jest.fn()
},
orders: {
create: jest.fn()
},
inventory: {
check: jest.fn(),
reserve: jest.fn()
}
};
StateSetClient.mockImplementation(() => mockClient);
orderProcessor = new OrderProcessor();
});
describe('processOrder', () => {
it('should create customer and order successfully', async () => {
const orderData = {
customer: {
email: 'test@example.com',
first_name: 'John',
last_name: 'Doe'
},
items: [
{ sku: 'ITEM-001', quantity: 1, price: 29.99 }
]
};
// Mock customer creation
mockClient.customers.create.mockResolvedValue({
id: 'cust_123',
email: 'test@example.com'
});
// Mock inventory check
mockClient.inventory.check.mockResolvedValue({
available: true,
items: [{ sku: 'ITEM-001', available: true, quantity: 10 }]
});
// Mock order creation
mockClient.orders.create.mockResolvedValue({
id: 'ord_123',
status: 'pending',
customer_id: 'cust_123'
});
const result = await orderProcessor.processOrder(orderData);
expect(mockClient.customers.create).toHaveBeenCalledWith(orderData.customer);
expect(mockClient.inventory.check).toHaveBeenCalledWith({
items: orderData.items
});
expect(mockClient.orders.create).toHaveBeenCalledWith({
customer_id: 'cust_123',
items: orderData.items
});
expect(result.orderId).toBe('ord_123');
expect(result.customerId).toBe('cust_123');
});
it('should handle insufficient inventory', async () => {
const orderData = {
customer: { email: 'test@example.com' },
items: [{ sku: 'ITEM-001', quantity: 5 }]
};
mockClient.inventory.check.mockResolvedValue({
available: false,
items: [{ sku: 'ITEM-001', available: false, quantity: 2 }]
});
await expect(orderProcessor.processOrder(orderData))
.rejects.toThrow('Insufficient inventory');
expect(mockClient.orders.create).not.toHaveBeenCalled();
});
it('should handle existing customer', async () => {
const orderData = {
customer: { email: 'existing@example.com' },
items: [{ sku: 'ITEM-001', quantity: 1 }]
};
// Customer creation fails with duplicate email
mockClient.customers.create.mockRejectedValue({
code: 'DUPLICATE_EMAIL',
details: { existing_customer_id: 'cust_existing' }
});
// Get existing customer
mockClient.customers.get.mockResolvedValue({
id: 'cust_existing',
email: 'existing@example.com'
});
mockClient.inventory.check.mockResolvedValue({ available: true });
mockClient.orders.create.mockResolvedValue({
id: 'ord_456',
customer_id: 'cust_existing'
});
const result = await orderProcessor.processOrder(orderData);
expect(mockClient.customers.get).toHaveBeenCalledWith('cust_existing');
expect(result.customerId).toBe('cust_existing');
});
});
});
// test/integration/customers.test.js
import { testClient, createTestData, testHelpers } from '../setup.js';
describe('Customer API Integration', () => {
const createdResources = [];
afterEach(async () => {
await testHelpers.cleanupResources(createdResources);
createdResources.length = 0;
});
describe('Customer lifecycle', () => {
it('should create, update, and delete customer', async () => {
// Create customer
const customerData = createTestData.customer();
const customer = await testClient.customers.create(customerData);
createdResources.push({
cleanup: () => testClient.customers.delete(customer.id)
});
expect(customer.id).toBeDefined();
expect(customer.email).toBe(customerData.email);
expect(customer.status).toBe('active');
// Update customer
const updateData = {
first_name: 'Updated',
customer_tier: 'gold'
};
const updatedCustomer = await testClient.customers.update(
customer.id,
updateData
);
expect(updatedCustomer.first_name).toBe('Updated');
expect(updatedCustomer.customer_tier).toBe('gold');
expect(updatedCustomer.email).toBe(customerData.email); // Should remain unchanged
// Get customer
const retrievedCustomer = await testClient.customers.get(customer.id);
expect(retrievedCustomer).toEqual(updatedCustomer);
// List customers (should include our customer)
const customers = await testClient.customers.list({
email: customerData.email
});
expect(customers.customers).toHaveLength(1);
expect(customers.customers[0].id).toBe(customer.id);
});
it('should prevent duplicate email addresses', async () => {
const customerData = createTestData.customer();
// Create first customer
const customer1 = await testClient.customers.create(customerData);
createdResources.push({
cleanup: () => testClient.customers.delete(customer1.id)
});
// Try to create second customer with same email
await expect(testClient.customers.create(customerData))
.rejects.toMatchObject({
code: 'DUPLICATE_EMAIL',
details: expect.objectContaining({
email: customerData.email,
existing_customer_id: customer1.id
})
});
});
it('should validate required fields', async () => {
const invalidData = {
email: 'invalid-email',
first_name: '', // Required but empty
last_name: 'Test'
};
await expect(testClient.customers.create(invalidData))
.rejects.toMatchObject({
code: 'VALIDATION_ERROR',
details: expect.objectContaining({
errors: expect.arrayContaining([
expect.objectContaining({
field: 'email',
message: expect.stringContaining('Invalid email format')
}),
expect.objectContaining({
field: 'first_name',
message: expect.stringContaining('required')
})
])
})
});
});
});
describe('Customer search and filtering', () => {
beforeEach(async () => {
// Create test customers with different tiers and statuses
const customers = [
{ ...createTestData.customer(), customer_tier: 'bronze', first_name: 'Alice' },
{ ...createTestData.customer(), customer_tier: 'gold', first_name: 'Bob' },
{ ...createTestData.customer(), customer_tier: 'platinum', first_name: 'Charlie' }
];
for (const customerData of customers) {
const customer = await testClient.customers.create(customerData);
createdResources.push({
cleanup: () => testClient.customers.delete(customer.id)
});
}
// Wait for eventual consistency
await testHelpers.waitFor(1000);
});
it('should filter customers by tier', async () => {
const goldCustomers = await testClient.customers.list({
customer_tier: 'gold'
});
expect(goldCustomers.customers).toHaveLength(1);
expect(goldCustomers.customers[0].first_name).toBe('Bob');
});
it('should search customers by name', async () => {
const searchResults = await testClient.customers.list({
search: 'Alice'
});
expect(searchResults.customers).toHaveLength(1);
expect(searchResults.customers[0].first_name).toBe('Alice');
});
it('should paginate results correctly', async () => {
const page1 = await testClient.customers.list({
limit: 2,
offset: 0
});
expect(page1.customers).toHaveLength(2);
expect(page1.pagination.has_more).toBe(true);
const page2 = await testClient.customers.list({
limit: 2,
offset: 2
});
expect(page2.customers).toHaveLength(1);
expect(page2.pagination.has_more).toBe(false);
// Ensure no overlap between pages
const page1Ids = page1.customers.map(c => c.id);
const page2Ids = page2.customers.map(c => c.id);
expect(page1Ids).not.toEqual(expect.arrayContaining(page2Ids));
});
});
});
// test/integration/orderWorkflow.test.js
import { testClient, createTestData, testHelpers } from '../setup.js';
describe('Order Workflow Integration', () => {
const createdResources = [];
afterEach(async () => {
await testHelpers.cleanupResources(createdResources);
createdResources.length = 0;
});
describe('Complete order lifecycle', () => {
it('should process order from creation to fulfillment', async () => {
// 1. Create customer
const customerData = createTestData.customer();
const customer = await testClient.customers.create(customerData);
createdResources.push({
cleanup: () => testClient.customers.delete(customer.id)
});
// 2. Create order
const orderData = createTestData.order(customer.id);
const order = await testClient.orders.create(orderData);
createdResources.push({
cleanup: () => testClient.orders.delete(order.id)
});
expect(order.status).toBe('pending');
expect(order.customer_id).toBe(customer.id);
// 3. Process payment (mock payment in sandbox)
const payment = await testClient.orders.pay(order.id, {
payment_method: {
type: 'card',
card: {
number: '4242424242424242', // Test card
exp_month: 12,
exp_year: 2025,
cvc: '123'
}
}
});
expect(payment.status).toBe('succeeded');
// 4. Check order status updated
const paidOrder = await testClient.orders.get(order.id);
expect(paidOrder.status).toBe('paid');
expect(paidOrder.payment_status).toBe('paid');
// 5. Ship order
const shipment = await testClient.orders.ship(order.id, {
carrier: 'fedex',
tracking_number: 'TEST123456789',
items: orderData.items
});
expect(shipment.tracking_number).toBe('TEST123456789');
// 6. Verify final status
const shippedOrder = await testClient.orders.get(order.id);
expect(shippedOrder.status).toBe('shipped');
expect(shippedOrder.tracking_number).toBe('TEST123456789');
// 7. Create return
const returnRequest = await testClient.returns.create({
order_id: order.id,
items: [
{
sku: orderData.items[0].sku,
quantity: 1,
reason: 'not_as_described'
}
],
customer_email: customer.email,
reason_code: 'not_satisfied'
});
expect(returnRequest.status).toBe('requested');
expect(returnRequest.order_id).toBe(order.id);
createdResources.push({
cleanup: () => testClient.returns.delete(returnRequest.id)
});
});
it('should handle inventory constraints', async () => {
const customer = await testClient.customers.create(createTestData.customer());
createdResources.push({
cleanup: () => testClient.customers.delete(customer.id)
});
// Try to order more items than available
const orderData = {
customer_id: customer.id,
items: [
{
sku: 'LIMITED-STOCK-ITEM',
quantity: 999999, // Exceeds available inventory
price: 29.99
}
]
};
await expect(testClient.orders.create(orderData))
.rejects.toMatchObject({
code: 'INSUFFICIENT_INVENTORY',
details: expect.objectContaining({
unavailable_items: expect.arrayContaining(['LIMITED-STOCK-ITEM'])
})
});
});
});
describe('Bulk operations', () => {
it('should handle bulk customer creation', async () => {
const customerBatch = Array.from({ length: 5 }, () => createTestData.customer());
const results = await Promise.all(
customerBatch.map(data => testClient.customers.create(data))
);
// Clean up created customers
results.forEach(customer => {
createdResources.push({
cleanup: () => testClient.customers.delete(customer.id)
});
});
expect(results).toHaveLength(5);
results.forEach((customer, index) => {
expect(customer.email).toBe(customerBatch[index].email);
expect(customer.id).toBeDefined();
});
});
it('should respect rate limits during bulk operations', async () => {
const customerBatch = Array.from({ length: 20 }, () => createTestData.customer());
// Create customers with proper rate limiting
const results = [];
for (const customerData of customerBatch) {
try {
const customer = await testClient.customers.create(customerData);
results.push(customer);
createdResources.push({
cleanup: () => testClient.customers.delete(customer.id)
});
// Small delay to avoid hitting rate limits
await testHelpers.waitFor(100);
} catch (error) {
if (error.code === 'RATE_LIMITED') {
// Wait for rate limit reset and retry
await testHelpers.waitFor(error.details.retry_after * 1000);
const customer = await testClient.customers.create(customerData);
results.push(customer);
} else {
throw error;
}
}
}
expect(results).toHaveLength(20);
});
});
});
// test/mocks/statesetMock.js
import { rest } from 'msw';
import { setupServer } from 'msw/node';
// Mock data store
const mockData = {
customers: new Map(),
orders: new Map(),
returns: new Map()
};
// ID generators
const generateId = (prefix) => `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2)}`;
// Mock API handlers
const handlers = [
// Customer endpoints
rest.post('https://api.sandbox.stateset.com/v1/customers', (req, res, ctx) => {
const customerData = req.body;
// Validate required fields
if (!customerData.email || !customerData.first_name || !customerData.last_name) {
return res(
ctx.status(400),
ctx.json({
error: {
code: 'VALIDATION_ERROR',
message: 'Missing required fields',
errors: [
...(customerData.email ? [] : [{ field: 'email', message: 'Email is required' }]),
...(customerData.first_name ? [] : [{ field: 'first_name', message: 'First name is required' }]),
...(customerData.last_name ? [] : [{ field: 'last_name', message: 'Last name is required' }])
]
}
})
);
}
// Check for duplicate email
const existingCustomer = Array.from(mockData.customers.values())
.find(c => c.email === customerData.email);
if (existingCustomer) {
return res(
ctx.status(409),
ctx.json({
error: {
code: 'DUPLICATE_EMAIL',
message: 'Customer with this email already exists',
details: {
email: customerData.email,
existing_customer_id: existingCustomer.id
}
}
})
);
}
// Create customer
const customer = {
id: generateId('cust'),
...customerData,
status: 'active',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
mockData.customers.set(customer.id, customer);
return res(
ctx.status(201),
ctx.json(customer)
);
}),
rest.get('https://api.sandbox.stateset.com/v1/customers/:id', (req, res, ctx) => {
const { id } = req.params;
const customer = mockData.customers.get(id);
if (!customer) {
return res(
ctx.status(404),
ctx.json({
error: {
code: 'RESOURCE_NOT_FOUND',
message: 'Customer not found'
}
})
);
}
return res(ctx.json(customer));
}),
rest.get('https://api.sandbox.stateset.com/v1/customers', (req, res, ctx) => {
const searchParams = req.url.searchParams;
const limit = parseInt(searchParams.get('limit')) || 20;
const offset = parseInt(searchParams.get('offset')) || 0;
const email = searchParams.get('email');
const tier = searchParams.get('customer_tier');
let customers = Array.from(mockData.customers.values());
// Apply filters
if (email) {
customers = customers.filter(c => c.email === email);
}
if (tier) {
customers = customers.filter(c => c.customer_tier === tier);
}
// Apply pagination
const totalCount = customers.length;
const paginatedCustomers = customers.slice(offset, offset + limit);
return res(
ctx.json({
customers: paginatedCustomers,
pagination: {
has_more: offset + limit < totalCount,
total_count: totalCount
}
})
);
}),
// Order endpoints
rest.post('https://api.sandbox.stateset.com/v1/orders', (req, res, ctx) => {
const orderData = req.body;
// Validate customer exists
if (!mockData.customers.has(orderData.customer_id)) {
return res(
ctx.status(400),
ctx.json({
error: {
code: 'INVALID_CUSTOMER',
message: 'Customer not found'
}
})
);
}
// Check inventory (simplified)
const hasLimitedStock = orderData.items.some(item =>
item.sku === 'LIMITED-STOCK-ITEM' && item.quantity > 10
);
if (hasLimitedStock) {
return res(
ctx.status(422),
ctx.json({
error: {
code: 'INSUFFICIENT_INVENTORY',
message: 'Insufficient inventory',
details: {
unavailable_items: ['LIMITED-STOCK-ITEM']
}
}
})
);
}
const order = {
id: generateId('ord'),
...orderData,
status: 'pending',
payment_status: 'pending',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
mockData.orders.set(order.id, order);
return res(
ctx.status(201),
ctx.json(order)
);
}),
// Rate limiting simulation
rest.use((req, res, ctx) => {
// Simulate rate limiting for high-frequency requests
const isRateLimited = Math.random() < 0.1; // 10% chance
if (isRateLimited && req.method === 'POST') {
return res(
ctx.status(429),
ctx.json({
error: {
code: 'RATE_LIMITED',
message: 'Rate limit exceeded',
details: {
retry_after: 2
}
}
})
);
}
})
];
// Create mock server
export const mockServer = setupServer(...handlers);
// Helper functions
export const mockHelpers = {
resetData: () => {
mockData.customers.clear();
mockData.orders.clear();
mockData.returns.clear();
},
addCustomer: (customer) => {
const id = customer.id || generateId('cust');
const fullCustomer = {
id,
status: 'active',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
...customer
};
mockData.customers.set(id, fullCustomer);
return fullCustomer;
},
getCustomers: () => Array.from(mockData.customers.values()),
getOrders: () => Array.from(mockData.orders.values())
};
// test/unit/customerService.mock.test.js
import { mockServer, mockHelpers } from '../mocks/statesetMock.js';
import { CustomerService } from '../../src/services/customerService.js';
describe('CustomerService with mocks', () => {
let customerService;
beforeAll(() => {
mockServer.listen();
});
beforeEach(() => {
mockHelpers.resetData();
customerService = new CustomerService({
apiKey: 'sk_test_mock',
baseUrl: 'https://api.sandbox.stateset.com/v1'
});
});
afterEach(() => {
mockServer.resetHandlers();
});
afterAll(() => {
mockServer.close();
});
it('should create and retrieve customer', async () => {
const customerData = {
email: 'test@example.com',
first_name: 'John',
last_name: 'Doe'
};
// Create customer
const customer = await customerService.createCustomer(customerData);
expect(customer.id).toBeDefined();
expect(customer.email).toBe(customerData.email);
// Retrieve customer
const retrievedCustomer = await customerService.getCustomer(customer.id);
expect(retrievedCustomer).toEqual(customer);
});
it('should handle validation errors', async () => {
const invalidData = {
email: 'test@example.com',
// Missing required fields
};
await expect(customerService.createCustomer(invalidData))
.rejects.toMatchObject({
code: 'VALIDATION_ERROR'
});
});
it('should handle duplicate emails', async () => {
const customerData = {
email: 'duplicate@example.com',
first_name: 'John',
last_name: 'Doe'
};
// Create first customer
await customerService.createCustomer(customerData);
// Try to create duplicate
await expect(customerService.createCustomer(customerData))
.rejects.toMatchObject({
code: 'DUPLICATE_EMAIL'
});
});
it('should paginate customer list', async () => {
// Create multiple customers
const customers = [];
for (let i = 0; i < 5; i++) {
customers.push(mockHelpers.addCustomer({
email: `test${i}@example.com`,
first_name: `Test${i}`,
last_name: 'User'
}));
}
// Get first page
const page1 = await customerService.listCustomers({ limit: 2, offset: 0 });
expect(page1.customers).toHaveLength(2);
expect(page1.pagination.has_more).toBe(true);
// Get second page
const page2 = await customerService.listCustomers({ limit: 2, offset: 2 });
expect(page2.customers).toHaveLength(2);
expect(page2.pagination.has_more).toBe(true);
// Get third page
const page3 = await customerService.listCustomers({ limit: 2, offset: 4 });
expect(page3.customers).toHaveLength(1);
expect(page3.pagination.has_more).toBe(false);
});
});
// test/e2e/setup.js
import { Browser, chromium } from '@playwright/test';
import { testClient } from '../setup.js';
export class E2ETestEnvironment {
constructor() {
this.browser = null;
this.context = null;
this.page = null;
this.baseUrl = process.env.E2E_BASE_URL || 'http://localhost:3000';
}
async setup() {
this.browser = await chromium.launch({
headless: process.env.CI === 'true'
});
this.context = await this.browser.newContext({
// Record videos for failed tests
recordVideo: {
dir: 'test-results/videos/',
size: { width: 1280, height: 720 }
}
});
this.page = await this.context.newPage();
// Setup API interception for debugging
this.page.on('request', request => {
if (request.url().includes('api.stateset.com')) {
console.log('API Request:', request.method(), request.url());
}
});
this.page.on('response', response => {
if (response.url().includes('api.stateset.com')) {
console.log('API Response:', response.status(), response.url());
}
});
}
async teardown() {
await this.page?.close();
await this.context?.close();
await this.browser?.close();
}
async createTestCustomer() {
const customerData = {
email: `e2e-test-${Date.now()}@example.com`,
first_name: 'E2E',
last_name: 'Test',
phone: '+1-555-123-4567'
};
return await testClient.customers.create(customerData);
}
async navigateTo(path) {
await this.page.goto(`${this.baseUrl}${path}`);
}
async waitForApiCall(urlPattern) {
return await this.page.waitForResponse(
response => response.url().includes(urlPattern) && response.status() === 200
);
}
}
// test/e2e/customerJourney.test.js
import { test, expect } from '@playwright/test';
import { E2ETestEnvironment } from './setup.js';
test.describe('Customer Journey E2E', () => {
let testEnv;
test.beforeEach(async () => {
testEnv = new E2ETestEnvironment();
await testEnv.setup();
});
test.afterEach(async () => {
await testEnv.teardown();
});
test('complete customer signup and first order', async () => {
const { page } = testEnv;
// 1. Navigate to signup page
await testEnv.navigateTo('/signup');
// 2. Fill out customer form
await page.fill('[data-testid="email"]', 'e2e-customer@example.com');
await page.fill('[data-testid="first-name"]', 'John');
await page.fill('[data-testid="last-name"]', 'Doe');
await page.fill('[data-testid="phone"]', '+1-555-123-4567');
// 3. Submit form and wait for API call
const createCustomerPromise = testEnv.waitForApiCall('/v1/customers');
await page.click('[data-testid="submit-button"]');
await createCustomerPromise;
// 4. Verify redirect to dashboard
await expect(page).toHaveURL(/\/dashboard/);
// 5. Check customer data is displayed
await expect(page.locator('[data-testid="customer-name"]')).toContainText('John Doe');
// 6. Navigate to products page
await page.click('[data-testid="products-link"]');
await expect(page).toHaveURL(/\/products/);
// 7. Add product to cart
await page.click('[data-testid="add-to-cart"]:first-of-type');
// 8. Verify cart updates
await expect(page.locator('[data-testid="cart-count"]')).toContainText('1');
// 9. Go to checkout
await page.click('[data-testid="cart-button"]');
await page.click('[data-testid="checkout-button"]');
// 10. Fill shipping address
await page.fill('[data-testid="street1"]', '123 Test Street');
await page.fill('[data-testid="city"]', 'Test City');
await page.selectOption('[data-testid="state"]', 'CA');
await page.fill('[data-testid="postal-code"]', '12345');
// 11. Enter payment information (test mode)
await page.fill('[data-testid="card-number"]', '4242424242424242');
await page.fill('[data-testid="card-expiry"]', '12/25');
await page.fill('[data-testid="card-cvc"]', '123');
// 12. Submit order
const createOrderPromise = testEnv.waitForApiCall('/v1/orders');
await page.click('[data-testid="place-order-button"]');
await createOrderPromise;
// 13. Verify order confirmation
await expect(page).toHaveURL(/\/order-confirmation/);
await expect(page.locator('[data-testid="order-number"]')).toBeVisible();
// 14. Extract order number for cleanup
const orderNumber = await page.locator('[data-testid="order-number"]').textContent();
console.log('Created order:', orderNumber);
});
test('customer support chat flow', async () => {
const { page } = testEnv;
// Create a customer first
const customer = await testEnv.createTestCustomer();
// 1. Navigate to support page with customer context
await testEnv.navigateTo(`/support?customer_id=${customer.id}`);
// 2. Start chat
await page.click('[data-testid="start-chat-button"]');
// 3. Send message
await page.fill('[data-testid="chat-input"]', 'I need help with my order');
await page.click('[data-testid="send-message"]');
// 4. Wait for AI response
await page.waitForSelector('[data-testid="ai-response"]', { timeout: 10000 });
// 5. Verify response appears
const response = await page.locator('[data-testid="ai-response"]').textContent();
expect(response.length).toBeGreaterThan(0);
// 6. Test escalation to human
await page.fill('[data-testid="chat-input"]', 'I want to speak to a human');
await page.click('[data-testid="send-message"]');
// 7. Verify escalation UI appears
await expect(page.locator('[data-testid="human-handoff"]')).toBeVisible();
});
test('return request flow', async () => {
const { page } = testEnv;
// Setup: Create customer and order
const customer = await testEnv.createTestCustomer();
// Navigate to returns page
await testEnv.navigateTo('/returns/create');
// 1. Enter order information
await page.fill('[data-testid="order-number"]', 'ORD-TEST-123');
await page.fill('[data-testid="email"]', customer.email);
// 2. Click lookup order
await page.click('[data-testid="lookup-order"]');
// 3. Select items to return
await page.check('[data-testid="return-item-0"]');
// 4. Select reason
await page.selectOption('[data-testid="return-reason"]', 'defective');
// 5. Add description
await page.fill('[data-testid="return-description"]', 'Product arrived damaged');
// 6. Submit return request
const createReturnPromise = testEnv.waitForApiCall('/v1/returns');
await page.click('[data-testid="submit-return"]');
await createReturnPromise;
// 7. Verify return confirmation
await expect(page.locator('[data-testid="return-number"]')).toBeVisible();
await expect(page.locator('[data-testid="return-status"]')).toContainText('Requested');
});
});
// test/performance/loadTest.js
import { check, sleep } from 'k6';
import http from 'k6/http';
export let options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up to 100 users
{ duration: '5m', target: 100 }, // Stay at 100 users
{ duration: '2m', target: 200 }, // Ramp up to 200 users
{ duration: '5m', target: 200 }, // Stay at 200 users
{ duration: '2m', target: 0 }, // Ramp down to 0 users
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
http_req_failed: ['rate<0.1'], // Less than 10% error rate
},
};
const API_BASE_URL = 'https://api.sandbox.stateset.com/v1';
const API_KEY = __ENV.STATESET_TEST_API_KEY;
export default function () {
const headers = {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
};
// Test customer creation
const customerData = {
email: `loadtest-${__VU}-${__ITER}@example.com`,
first_name: 'Load',
last_name: 'Test',
};
const createResponse = http.post(
`${API_BASE_URL}/customers`,
JSON.stringify(customerData),
{ headers }
);
check(createResponse, {
'customer creation status is 201': (r) => r.status === 201,
'customer creation response time < 500ms': (r) => r.timings.duration < 500,
});
if (createResponse.status === 201) {
const customer = JSON.parse(createResponse.body);
// Test customer retrieval
const getResponse = http.get(
`${API_BASE_URL}/customers/${customer.id}`,
{ headers }
);
check(getResponse, {
'customer retrieval status is 200': (r) => r.status === 200,
'customer retrieval response time < 200ms': (r) => r.timings.duration < 200,
});
// Test customer list
const listResponse = http.get(
`${API_BASE_URL}/customers?limit=10`,
{ headers }
);
check(listResponse, {
'customer list status is 200': (r) => r.status === 200,
'customer list response time < 300ms': (r) => r.timings.duration < 300,
});
}
sleep(1);
}
# .github/workflows/api-tests.yml
name: API Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
env:
NODE_ENV: test
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run integration tests
run: npm run test:integration
env:
STATESET_TEST_API_KEY: ${{ secrets.STATESET_TEST_API_KEY }}
NODE_ENV: test
- name: Upload test results
uses: actions/upload-artifact@v3
if: failure()
with:
name: integration-test-results
path: test-results/
e2e-tests:
runs-on: ubuntu-latest
needs: integration-tests
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Start application
run: |
npm run build
npm run start &
sleep 10
env:
NODE_ENV: test
- name: Run E2E tests
run: npm run test:e2e
env:
STATESET_TEST_API_KEY: ${{ secrets.STATESET_TEST_API_KEY }}
E2E_BASE_URL: http://localhost:3000
- name: Upload E2E results
uses: actions/upload-artifact@v3
if: failure()
with:
name: e2e-test-results
path: test-results/
performance-tests:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Run k6 performance tests
uses: grafana/k6-action@v0.3.0
with:
filename: test/performance/loadTest.js
env:
STATESET_TEST_API_KEY: ${{ secrets.STATESET_TEST_API_KEY }}
// test/factories/testDataFactory.js
import { faker } from '@faker-js/faker';
export class TestDataFactory {
static customer(overrides = {}) {
return {
email: faker.internet.email(),
first_name: faker.person.firstName(),
last_name: faker.person.lastName(),
phone: faker.phone.number('+1-###-###-####'),
date_of_birth: faker.date.birthdate({ min: 18, max: 80, mode: 'age' }).toISOString().split('T')[0],
address: this.address(),
customer_tier: faker.helpers.arrayElement(['bronze', 'silver', 'gold', 'platinum']),
marketing_consent: faker.datatype.boolean(),
...overrides
};
}
static address(overrides = {}) {
return {
street1: faker.location.streetAddress(),
street2: faker.helpers.maybe(() => faker.location.secondaryAddress(), 0.3),
city: faker.location.city(),
state: faker.location.state({ abbreviated: true }),
postal_code: faker.location.zipCode(),
country: 'US',
...overrides
};
}
static order(customerId, overrides = {}) {
return {
customer_id: customerId,
items: [this.orderItem()],
currency: 'USD',
shipping_address: this.address(),
billing_address: this.address(),
...overrides
};
}
static orderItem(overrides = {}) {
return {
sku: faker.helpers.arrayElement(['WIDGET-001', 'GADGET-002', 'TOOL-003']),
quantity: faker.number.int({ min: 1, max: 5 }),
price: parseFloat(faker.commerce.price({ min: 10, max: 100, dec: 2 })),
...overrides
};
}
static returnRequest(orderId, overrides = {}) {
return {
order_id: orderId,
reason_code: faker.helpers.arrayElement(['defective', 'not_satisfied', 'wrong_item']),
customer_notes: faker.lorem.sentence(),
items: [this.returnItem()],
...overrides
};
}
static returnItem(overrides = {}) {
return {
sku: faker.helpers.arrayElement(['WIDGET-001', 'GADGET-002', 'TOOL-003']),
quantity: faker.number.int({ min: 1, max: 3 }),
reason: faker.helpers.arrayElement(['defective', 'not_as_described', 'wrong_item']),
condition: faker.helpers.arrayElement(['A', 'B', 'C']),
...overrides
};
}
// Scenario-based data generation
static scenarioData = {
// High-value enterprise customer
enterpriseCustomer: () => this.customer({
customer_tier: 'platinum',
custom_fields: {
company_name: faker.company.name(),
industry: 'Technology',
company_size: '500+'
}
}),
// Problem order for testing returns
problematicOrder: (customerId) => this.order(customerId, {
items: [
this.orderItem({ sku: 'DEFECTIVE-ITEM', quantity: 1 })
]
}),
// Bulk order
bulkOrder: (customerId) => this.order(customerId, {
items: Array.from({ length: 5 }, () => this.orderItem())
})
};
}