Building with Passport
Stamps
Create a Stamp
Testing & 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