>_

Testing

The Open Privacy Suite testbed supports three testing modes:

  1. Unit / Integration Tests (Go) -- Use mock servers and testcontainers for fast, deterministic testing.
  2. E2E Tests (Go-based and Playwright) -- Use real Anvil node for comprehensive API testing.
  3. Demo acceptance E2E -- Runs the proxy, indexer, admin UI, and privacy-mode block explorer together and verifies the demo-critical security behavior end to end.

Key design decisions:

  • Mock Ethereum node for fast unit/integration tests
  • Anvil (Foundry) for realistic E2E tests
  • Mock Billions service always returns KYC=true
  • testcontainers-go for automatic PostgreSQL in unit tests
  • Docker Compose for full E2E testing
  • Playwright for parallel API test execution

Docker Compose Services

ServiceImagePortPurpose
postgrespostgres:15-alpine5432Policy and token storage
anvilfoundry8545Local Ethereum node
proxy-backendGo binary8080Open Privacy Suite server
proxy-frontendNode.js5173Admin UI (Vite dev server)
chain-indexerPublished imageinternal onlyIndexes real Anvil blocks for privacy APIs
block-explorer-apiSibling repositoryinternal onlyPrivacy-mode explorer BFF
block-explorer-frontendSibling repository3001Explorer UI

Service dependency chain:

All services wait for their dependencies to be healthy before starting.

Unit Tests with testcontainers-go

Unit tests use testcontainers-go to automatically spin up PostgreSQL containers. No manual database setup is required.

func SetupTestContainer(t *testing.T) (string, func()) {
    postgresContainer, err := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:15-alpine"),
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("testuser"),
        postgres.WithPassword("testpass"),
    )

    // If Docker unavailable, fall back to external PostgreSQL
    if err != nil {
        dbURL := "postgres://postgres:postgres@localhost:5432/privacy_proxy_test?sslmode=disable"
        return dbURL, func() {}
    }

    return connStr, cleanup
}

Fallback behavior: If testcontainers fails (Docker not available, network issues), tests fall back to external PostgreSQL at localhost:5432. Set TEST_DATABASE_URL to override.

E2E Tests (Go-based)

Located in e2e/proxy_test.go. These tests verify the full authentication and authorization flow using a mock Privado verifier:

type mockPrivadoVerifier struct {
    userDID string
}

func (m *mockPrivadoVerifier) VerifyJWZ(...) (string, error) {
    return m.userDID, nil  // Always succeeds with configured DID
}
TestDescription
TestE2E_Proxy_JSONRPCWithAuthUser with valid policy can make allowed calls
TestE2E_UnauthorizedRequest_NoTokenRequest without JWT returns an opaque 404
TestE2E_ForbiddenRequest_DisallowedMethodCalling a non-allowlisted method returns an opaque 404
TestE2E_BannedUserBanned user returns an opaque 404
TestE2E_NoKYCUser without KYC returns an opaque 404

E2E Tests (Playwright)

Located in e2e/playwright/. Playwright-based E2E tests run against a real Docker environment with Anvil.

Directory Structure

e2e/playwright/
├── package.json
├── playwright.config.ts
├── tsconfig.json
├── Dockerfile
├── global-setup.ts
├── helpers/            # auth.ts, policy.ts, address.ts, rbac-api.ts, rbac-fixtures.ts, test-context.ts
└── tests/
    ├── ui/             # numbered specs: 01-auth, 02-navigation, 03-organizations,
                        # 05-test-request, 06-admin-auth, 07-batch-operations,
                        # 08-event-rules, 09-visibleto-caption, 10-disclosure-scope,
                        # 11-compliance-unsaved-changes, 12-access-logs-denial-reasons,
                        # 13-compliance-monitor-mode (+ compliance/03-currency-selector-ui)
    └── demo/           # cross-product acceptance: 00-setup, 00-cleanup, 10-rbac-explorer,
                        # 20-disclosure, 30-travel-rule, 40-view-as, 50-explorer-conformance,
                        # 60-transaction-matrix

Demo acceptance coverage

The demo project is a cross-product privacy contract, not a presence-only smoke test. It creates two organizations and ten user personas, deploys real Counter and ERC-20 contracts, configures method/function/event grants, writes indexed transactions, approves full/pseudonymous/redacted disclosures, and configures travel-rule prices.

Each story compares the proxy RPC result, explorer BFF JSON, and rendered browser view where applicable. It asserts exact rows, counters, labels, log counts, addresses, amounts, and disclosure representations. Protected addresses, transaction hashes, and calldata are also used as canaries: tests fail if any forbidden value appears in a raw response, search result, or page. See the executable story catalog for the complete acceptance matrix and maintenance rules.

Test Isolation

Each test gets a unique DID using TestContext:

test('my test', async ({ request }) => {
  const ctx = new TestContext();
  // ctx.userDID = 'did:privado:test_a1b2c3d4'

  await ctx.createPolicy(request, { kyc: true });
  const token = await ctx.getToken(request);
  // Use token for API requests...

  await ctx.cleanup(request);
});

Running Tests

Unit Tests

# Run all unit tests (auto-starts PostgreSQL container)
make test-unit

# Or directly:
go test ./internal/...

E2E Tests (Go)

# Run E2E tests (requires Docker for testcontainers)
make test-e2e

# Or directly:
go test ./e2e/...

# With external PostgreSQL (for CI):
export TEST_DATABASE_URL="postgres://postgres:postgres@localhost:5432/test?sslmode=disable"
go test ./e2e/...

E2E Tests (Playwright)

# Run full E2E suite (starts services, runs tests, stops services)
make e2e

# Debug mode (services stay running)
make e2e-debug

# Stop E2E services
make e2e-down

Demo acceptance E2E

The demo suite requires Docker, Docker Compose, Foundry, and a sibling explorer checkout. By default the explorer is resolved at ../block-explorer; use BLOCK_EXPLORER_PATH for a different checkout or worktree.

# Build contracts, start the full isolated stack, run all acceptance stories,
# and always remove containers and volumes.
make demo-e2e

# Run against an explicit explorer checkout.
make demo-e2e BLOCK_EXPLORER_PATH=/absolute/path/to/block-explorer

# Keep the stack for Playwright Inspector, then tear it down explicitly.
make demo-e2e-debug BLOCK_EXPLORER_PATH=/absolute/path/to/block-explorer
make demo-e2e-down BLOCK_EXPLORER_PATH=/absolute/path/to/block-explorer

The acceptance project is serial and has no retries. A missing service, fixture, or expected redaction is a hard failure. Its temporary scenario manifest is not retained because it contains synthetic session tokens.

Test reports are generated at:

  • HTML report: e2e/playwright/playwright-report/index.html
  • JUnit XML: e2e/playwright/test-results/junit.xml

Full Docker Environment

# Start all services
make quickstart

# View logs
docker-compose logs -f proxy-backend

# Stop all services
make quickstart-down

Writing New Tests

Basic E2E Test Template (Go)

func TestE2E_MyFeature(t *testing.T) {
    userDID := "did:privado:test_user"

    // Setup server with mock verifier
    mockVerifier := &mockPrivadoVerifier{userDID: userDID}
    srv, serverURL, cleanup := setupE2EWithVerifier(t, mockVerifier)
    defer cleanup()

    // Get database and run migrations
    database := srv.DB()
    database.Migrate()

    // Create policy for user
    policy := &db.AccessPolicy{
        ExternalID:   userDID,
        KYC:          true,
        AllowMethods: []string{"eth_call"},
        Banned:       false,
    }
    database.SetPolicy(policy)

    // Get JWT token
    accessToken := getJWTToken(t, serverURL, userDID)

    // Make request
    reqBody := map[string]interface{}{
        "jsonrpc": "2.0",
        "method":  "eth_call",
        "params":  []interface{}{},
        "id":      1,
    }
    jsonBody, _ := json.Marshal(reqBody)

    req, _ := http.NewRequest("POST", serverURL+"/", bytes.NewReader(jsonBody))
    req.Header.Set("Authorization", "Bearer "+accessToken)
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{Timeout: 5 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        t.Fatalf("request failed: %v", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        t.Errorf("expected 200, got %d", resp.StatusCode)
    }
}

CI Integration

GitHub Actions Example

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15-alpine
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'

      - name: Run tests
        env:
          TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/test?sslmode=disable
        run: |
          go test ./internal/...
          go test ./e2e/...

Troubleshooting

Docker Issues

# Check if containers are running
docker-compose ps

# View container logs
docker-compose logs postgres
docker-compose logs proxy-backend

# Restart a specific service
docker-compose restart proxy-backend

Database Issues

# Connect to database
docker-compose exec postgres psql -U postgres -d privacy_proxy

# Reset database (destroys all data)
docker-compose down -v
docker-compose up -d

Test Failures

# Run with verbose output
go test -v ./e2e/...

# Run specific test
go test -v -run TestE2E_Proxy_JSONRPCWithAuth ./e2e/...

Service URLs When Running Docker

ServiceURL
Open Privacy Suite APIhttp://localhost:8080
Admin UIhttp://localhost:5173
Anvil (Ethereum Node)http://localhost:8545
PostgreSQLlocalhost:5432