Testing & Security
This guide covers comprehensive testing requirements and security best practices for Human Passport Stamp development.
Testing Requirements
All Stamp integrations must include a comprehensive test suite with >80% code coverage. PRs will be rejected without proper tests.
Test Suite Structure
Every Stamp must include tests in __tests__/yourProvider.test.ts
:
import { YourProvider } from "../Providers/yourProvider.js";
import { RequestPayload } from "@gitcoin/passport-types";
import axios from "axios";
jest.mock("axios");
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe("YourProvider", () => {
let provider: YourProvider;
beforeEach(() => {
provider = new YourProvider();
jest.clearAllMocks();
// Set required environment variables
process.env.YOURPLATFORM_CLIENT_ID = "test_client_id";
process.env.YOURPLATFORM_CLIENT_SECRET = "test_client_secret";
});
describe("verify method", () => {
const validPayload: RequestPayload = {
address: "0x1234567890123456789012345678901234567890",
proofs: { code: "valid_auth_code" },
type: "YourProviderName",
version: "0.0.0",
};
it("should return valid payload for successful verification", async () => {
// Test implementation
});
it("should return invalid payload for failed verification", async () => {
// Test implementation
});
it("should handle external API errors", async () => {
// Test implementation
});
});
});
Required Test Cases
1. Success Scenarios
Test all paths that should result in valid: true
:
it("should verify valid user with all requirements met", async () => {
// Mock successful OAuth flow
mockedAxios.post.mockResolvedValueOnce({
data: { access_token: "mock_token" },
});
mockedAxios.get.mockResolvedValueOnce({
data: {
id: "user123",
username: "testuser",
email_verified: true,
reputation: 150,
account_created: "2023-01-01T00:00:00Z",
status: "active",
},
});
const result = await provider.verify(validPayload);
expect(result.valid).toBe(true);
expect(result.record).toMatchObject({
id: "user123",
username: "testuser",
reputation: 150,
});
expect(result.errors).toBeUndefined();
});
2. Failure Scenarios
Test all validation rules and edge cases:
it("should reject user with insufficient reputation", async () => {
mockedAxios.post.mockResolvedValueOnce({
data: { access_token: "mock_token" },
});
mockedAxios.get.mockResolvedValueOnce({
data: {
id: "user123",
email_verified: true,
reputation: 50, // Below minimum requirement
account_created: "2023-01-01T00:00:00Z",
status: "active",
},
});
const result = await provider.verify(validPayload);
expect(result.valid).toBe(false);
expect(result.errors).toContain("Must have at least 100 reputation points");
});
it("should reject user with unverified email", async () => {
// Mock successful token but unverified email
mockedAxios.post.mockResolvedValueOnce({
data: { access_token: "mock_token" },
});
mockedAxios.get.mockResolvedValueOnce({
data: {
id: "user123",
email_verified: false, // Not verified
reputation: 150,
account_created: "2023-01-01T00:00:00Z",
status: "active",
},
});
const result = await provider.verify(validPayload);
expect(result.valid).toBe(false);
expect(result.errors).toContain("Email address must be verified");
});
3. Error Handling
Test external API failures and network issues:
it("should throw ProviderExternalVerificationError for OAuth failures", async () => {
mockedAxios.post.mockRejectedValueOnce({
response: {
status: 400,
data: { error: "invalid_grant" },
},
});
await expect(provider.verify(validPayload)).rejects.toThrow(
"YourPlatform verification error"
);
});
it("should handle API rate limiting", async () => {
mockedAxios.post.mockResolvedValueOnce({
data: { access_token: "mock_token" },
});
mockedAxios.get.mockRejectedValueOnce({
response: {
status: 429,
data: { error: "rate_limit_exceeded" },
},
});
await expect(provider.verify(validPayload)).rejects.toThrow();
});
it("should handle malformed API responses", async () => {
mockedAxios.post.mockResolvedValueOnce({
data: { access_token: "mock_token" },
});
mockedAxios.get.mockResolvedValueOnce({
data: null, // Malformed response
});
const result = await provider.verify(validPayload);
expect(result.valid).toBe(false);
expect(result.errors).toBeDefined();
});
4. Edge Cases
Test boundary conditions and unusual scenarios:
it("should handle account exactly at minimum age requirement", async () => {
const exactlyThirtyDaysAgo = new Date(Date.now() - (30 * 24 * 60 * 60 * 1000)).toISOString();
mockedAxios.post.mockResolvedValueOnce({
data: { access_token: "mock_token" },
});
mockedAxios.get.mockResolvedValueOnce({
data: {
id: "user123",
email_verified: true,
reputation: 150,
account_created: exactlyThirtyDaysAgo,
status: "active",
},
});
const result = await provider.verify(validPayload);
expect(result.valid).toBe(true);
});
it("should handle empty or missing user data fields", async () => {
mockedAxios.post.mockResolvedValueOnce({
data: { access_token: "mock_token" },
});
mockedAxios.get.mockResolvedValueOnce({
data: {
id: "", // Empty ID
username: null, // Null username
},
});
const result = await provider.verify(validPayload);
expect(result.valid).toBe(false);
});
Running Tests
Test your implementation with these commands:
# Run specific test file
yarn test platforms/src/YourPlatform/__tests__/yourProvider.test.ts
# Run with coverage
yarn test --coverage platforms/src/YourPlatform/
# Run in watch mode during development
yarn test --watch platforms/src/YourPlatform/
Security Best Practices
Security is critical for Stamp integrations. Follow these guidelines to protect user data and prevent vulnerabilities.
1. Data Protection
Never Log Sensitive Information
// ❌ BAD - Logs sensitive data
console.log("Access token:", accessToken);
console.log("User data:", userData);
// ✅ GOOD - Safe logging
console.log("Token received, length:", accessToken?.length || 0);
console.log("User verification completed for ID:", userData.id);
Validate All External Data
// ✅ Always validate API responses
const validateUserData = (data: any): boolean => {
return (
data &&
typeof data.id === 'string' &&
typeof data.verified === 'boolean' &&
data.id.length > 0
);
};
const userData = await this.getUserData(accessToken);
if (!validateUserData(userData)) {
return {
valid: false,
errors: ["Invalid user data received from platform"],
};
}
Secure Record Storage
// ✅ Only store necessary, non-sensitive data
return {
valid: true,
record: {
id: userData.id, // Platform user ID (not email)
username: userData.username, // Public username only
verificationLevel: userData.level, // Verification tier
verifiedAt: Date.now(), // Timestamp
// ❌ DON'T store: email, phone, address, tokens, etc.
},
};
2. API Security
Implement Rate Limiting Protection
const makeAPICall = async (url: string, options: any, retries = 3): Promise<any> => {
try {
return await axios(url, {
...options,
timeout: 10000, // 10 second timeout
});
} catch (error) {
if (error.response?.status === 429 && retries > 0) {
// Exponential backoff for rate limiting
const delay = Math.pow(2, 4 - retries) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return makeAPICall(url, options, retries - 1);
}
throw error;
}
};
Secure Error Messages
// ❌ BAD - Exposes internal details
return {
valid: false,
errors: [`Database error: ${dbError.message}`]
};
// ❌ BAD - Exposes API structure
return {
valid: false,
errors: [`API call to /internal/user/${userId} failed`]
};
// ✅ GOOD - User-friendly, no internal details
return {
valid: false,
errors: ["Unable to verify account. Please try again later."]
};
3. OAuth Security
Validate OAuth Scopes
private validateOAuthScopes(tokenResponse: any): boolean {
const requiredScopes = ['read:user', 'read:email'];
const grantedScopes = tokenResponse.scope?.split(' ') || [];
return requiredScopes.every(scope => grantedScopes.includes(scope));
}
// Use in token exchange
const tokenResponse = await this.getAccessToken(code);
if (!this.validateOAuthScopes(tokenResponse)) {
throw new Error("Insufficient OAuth scopes granted");
}
Secure Token Storage
// ❌ BAD - Token stored in memory too long
class BadProvider {
private accessToken: string; // Don't store tokens as instance variables
async verify() {
this.accessToken = await this.getAccessToken(code);
// Token persists in memory
}
}
// ✅ GOOD - Minimal token lifetime
class GoodProvider {
async verify(payload: RequestPayload) {
const accessToken = await this.getAccessToken(payload.proofs.code);
const userData = await this.getUserData(accessToken);
// Token goes out of scope immediately
return this.validateUser(userData);
}
}
4. On-Chain Security
Multiple RPC Endpoints
const RPC_ENDPOINTS = [
"https://mainnet.infura.io/v3/YOUR_PROJECT_ID",
"https://mainnet.alchemyapi.io/v2/YOUR_API_KEY",
"https://rpc.ankr.com/eth",
];
private async getRobustProvider(): Promise<any> {
for (const rpc of RPC_ENDPOINTS) {
try {
const provider = getRPCProvider(rpc);
// Test connection
await provider.getBlockNumber();
return provider;
} catch (error) {
continue; // Try next endpoint
}
}
throw new Error("No RPC endpoints available");
}
Contract Call Validation
private async safeContractCall(contract: Contract, method: string, ...args: any[]): Promise<any> {
try {
const result = await contract[method](...args);
// Validate result based on expected type
if (method === 'balanceOf' && typeof result !== 'bigint') {
throw new Error("Invalid balance response");
}
return result;
} catch (error) {
if (error.message.includes("execution reverted")) {
throw new Error("Contract call reverted - invalid parameters or state");
}
throw error;
}
}
5. Anti-Sybil Measures
Implement multiple checks to prevent Sybil attacks:
private async validateAntiSybil(address: string, userData: any): Promise<string[]> {
const errors: string[] = [];
// 1. Account age requirement
const accountAge = Date.now() - new Date(userData.created).getTime();
const minAge = 30 * 24 * 60 * 60 * 1000; // 30 days
if (accountAge < minAge) {
errors.push("Account too new");
}
// 2. Activity requirement
if (userData.activity_score < 100) {
errors.push("Insufficient platform activity");
}
// 3. Verification requirement
if (!userData.email_verified || !userData.phone_verified) {
errors.push("Account not fully verified");
}
// 4. On-chain history (for EVM stamps)
if (this.isEVM) {
const txCount = await provider.getTransactionCount(address);
if (txCount === 0) {
errors.push("No transaction history");
}
}
return errors;
}
Troubleshooting
Common Issues
OAuth Flow Problems
Issue: redirect_uri_mismatch
- Cause: OAuth redirect URI doesn't match registered URI exactly
- Solution: Ensure exact match including protocol, domain, path, and trailing slashes
- Debug: Log the constructed OAuth URL and compare with registered URIs
Issue: invalid_grant
- Cause: Authorization code expired, already used, or invalid
- Solution: Implement proper error handling and direct users to restart OAuth flow
- Prevention: Don't cache or reuse authorization codes
Issue: Token exchange fails
- Cause: Incorrect client credentials or malformed request
- Solution: Verify environment variables and request format
- Debug: Check API documentation for exact token exchange format
Integration Issues
Issue: Provider not appearing in UI
- Checklist:
- Platform registered in
platforms/src/platforms.ts
- App config added to
app/config/platformMap.ts
- PROVIDER_ID added to
types/src/index.d.ts
- Feature flag enabled (if applicable)
- Environment variables set correctly
- Platform registered in
Issue: Tests failing
- Common causes:
- External dependencies not mocked properly
- Environment variables not set in test environment
- Async/await handling issues
- TypeScript type mismatches
Issue: Verification always fails
- Debug steps:
- Check API endpoint accessibility
- Verify authentication credentials
- Test API calls manually with curl/Postman
- Check for rate limiting
- Validate request/response format
On-Chain Issues
Issue: Contract calls failing
- Causes:
- Wrong contract address
- Incorrect ABI
- Network connectivity issues
- Invalid method parameters
Issue: Balance checks incorrect
- Causes:
- Wrong token decimals
- Incorrect unit conversion
- Contract not implementing standard interface
Debug Tools
Enable Debug Logging
const DEBUG = process.env.NODE_ENV === 'development';
if (DEBUG) {
console.log('OAuth URL:', oauthUrl);
console.log('Token exchange request:', {
client_id: clientId.substring(0, 8) + '...',
grant_type: grantType
});
console.log('User data received, fields:', Object.keys(userData));
}
Manual API Testing
# Test OAuth URL generation
curl -X GET "https://api.yourplatform.com/oauth2/authorize?client_id=test&response_type=code&redirect_uri=http://localhost:3000/callback"
# Test token exchange
curl -X POST https://api.yourplatform.com/oauth2/token \
-H "Content-Type: application/json" \
-d '{"grant_type":"authorization_code","code":"test_code","client_id":"your_id","client_secret":"your_secret"}'
# Test user data endpoint
curl -X GET https://api.yourplatform.com/user/me \
-H "Authorization: Bearer YOUR_TEST_TOKEN"
Contract Debugging
# Check contract exists
curl -X POST https://mainnet.infura.io/v3/YOUR_PROJECT_ID \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_getCode","params":["0xCONTRACT_ADDRESS","latest"],"id":1}'
# Test balance call
curl -X POST https://mainnet.infura.io/v3/YOUR_PROJECT_ID \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0xCONTRACT_ADDRESS","data":"0x70a08231000000000000000000000000USER_ADDRESS"},"latest"],"id":1}'
Security Checklist
Before submitting your PR, verify:
Code Security
- No sensitive data logged to console
- All external API responses validated
- Proper error handling without internal detail exposure
- No hardcoded secrets or API keys
- Secure record data (no PII stored)
OAuth Security (if applicable)
- OAuth scopes properly validated
- Access tokens not stored as instance variables
- Proper handling of expired/invalid tokens
- Redirect URI validation implemented
On-Chain Security (if applicable)
- Multiple RPC endpoints configured
- Contract call validation implemented
- Proper unit conversion (decimals handling)
- Anti-sybil measures (transaction history checks)
Testing Security
- All success scenarios tested
- All failure scenarios tested
- Error handling tested
- Edge cases tested
- >80% code coverage achieved
Anti-Sybil Measures
- Account age requirements
- Activity/reputation requirements
- Email/phone verification checks
- Transaction history validation (for on-chain)
- Multiple validation criteria implemented