Skip to Content
Building with PassportStampsCreate a StampTesting & Security

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

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:
    1. Check API endpoint accessibility
    2. Verify authentication credentials
    3. Test API calls manually with curl/Postman
    4. Check for rate limiting
    5. 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
Last updated on