> For the complete documentation index, see [llms.txt](https://docs.astralbeam.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.astralbeam.io/for-developers/testing.md).

# Testing

This guide covers running and writing tests for AstralBeam components.

## Test Structure

```
bridge/
├── cli/
│   └── tests/
│       ├── unit/              # Unit tests
│       ├── integration/       # Integration tests
│       └── setup.ts           # Test configuration
├── contracts/
│   ├── eth/
│   │   └── test/             # Foundry tests
│   └── casper/
│       └── src/tests/        # Odra tests
```

***

## TypeScript Tests

### Running Tests

```bash
cd cli

# Run all tests
npm test

# Run with coverage
npm run test:coverage

# Run specific test file
npm test -- tests/integration/evm2casper.test.ts

# Run tests matching pattern
npm test -- --testNamePattern="lock tokens"

# Watch mode
npm test -- --watch
```

### Test Configuration

Tests use Jest with the following configuration (`jest.config.js`):

```javascript
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testTimeout: 120000,  // 2 minutes for integration tests
  setupFilesAfterEnv: ['./tests/setup.ts'],
  testMatch: [
    '**/tests/**/*.test.ts'
  ],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  }
};
```

### Writing Unit Tests

```typescript
// tests/unit/crypto.test.ts
import { computeLockAttestationHash } from '@/shared/crypto';

describe('Crypto Utils', () => {
  describe('computeLockAttestationHash', () => {
    it('should compute correct hash for lock attestation', () => {
      const attestation = {
        sourceChainId: 11155111n,
        evmBlockNumber: 1000000n,
        evmTxIndex: 0n,
        evmLogIndex: 0,
        token: '0x1234567890123456789012345678901234567890',
        sender: '0xabcdef1234567890abcdef1234567890abcdef12',
        casperRecipient: '0x01abc...',
        amount: 1000000n,
        nonce: 1n,
        receiptsRoot: '0x...'
      };

      const hash = computeLockAttestationHash(attestation);

      expect(hash).toMatch(/^0x[a-f0-9]{64}$/);
    });

    it('should produce different hashes for different chain IDs', () => {
      const baseAttestation = { /* ... */ };

      const hash1 = computeLockAttestationHash({
        ...baseAttestation,
        sourceChainId: 11155111n
      });

      const hash2 = computeLockAttestationHash({
        ...baseAttestation,
        sourceChainId: 84532n
      });

      expect(hash1).not.toEqual(hash2);
    });
  });
});
```

### Writing Integration Tests

```typescript
// tests/integration/evm2casper.test.ts
import { ethers } from 'ethers';
import { setupTestEnvironment, cleanupTestEnvironment } from './helpers';

describe('EVM to Casper Bridge', () => {
  let provider: ethers.Provider;
  let signer: ethers.Signer;
  let locker: ethers.Contract;
  let testToken: ethers.Contract;

  beforeAll(async () => {
    const env = await setupTestEnvironment();
    provider = env.provider;
    signer = env.signer;
    locker = env.locker;
    testToken = env.testToken;
  });

  afterAll(async () => {
    await cleanupTestEnvironment();
  });

  describe('Lock Flow', () => {
    it('should lock tokens and emit event', async () => {
      const amount = ethers.parseUnits('100', 6);
      const recipient = ethers.zeroPadValue('0x01abc...', 32);

      // Approve tokens
      await testToken.approve(locker.target, amount);

      // Get bridge fee
      const fee = await locker.bridgeFee();

      // Lock tokens
      const tx = await locker.lock(
        testToken.target,
        amount,
        recipient,
        { value: fee }
      );

      const receipt = await tx.wait();

      // Verify event
      const lockEvent = receipt.logs.find(
        log => log.topics[0] === locker.interface.getEvent('Lock').topicHash
      );

      expect(lockEvent).toBeDefined();

      const decoded = locker.interface.parseLog(lockEvent);
      expect(decoded.args.amount).toEqual(amount);
    });

    it('should reject lock without sufficient approval', async () => {
      const amount = ethers.parseUnits('100', 6);
      const recipient = ethers.zeroPadValue('0x01abc...', 32);
      const fee = await locker.bridgeFee();

      // Don't approve tokens

      await expect(
        locker.lock(testToken.target, amount, recipient, { value: fee })
      ).rejects.toThrow();
    });
  });
});
```

### Test Helpers

```typescript
// tests/integration/helpers.ts
import { ethers } from 'ethers';

export async function setupTestEnvironment() {
  const provider = new ethers.JsonRpcProvider(process.env.TEST_RPC_URL);
  const signer = new ethers.Wallet(process.env.TEST_PRIVATE_KEY, provider);

  const locker = new ethers.Contract(
    process.env.TEST_LOCKER_ADDRESS,
    LOCKER_ABI,
    signer
  );

  const testToken = new ethers.Contract(
    process.env.TEST_TOKEN_ADDRESS,
    ERC20_ABI,
    signer
  );

  // Mint test tokens
  await testToken.mint(signer.address, ethers.parseUnits('10000', 6));

  return { provider, signer, locker, testToken };
}

export async function waitForBridgeCompletion(
  txHash: string,
  timeout: number = 120000
): Promise<void> {
  const startTime = Date.now();

  while (Date.now() - startTime < timeout) {
    const response = await fetch(
      `${process.env.API_URL}/api/v1/transactions/${txHash}`
    );
    const tx = await response.json();

    if (tx.status === 'completed') {
      return;
    }

    if (tx.status === 'failed') {
      throw new Error(`Bridge failed: ${tx.error}`);
    }

    await new Promise(r => setTimeout(r, 5000));
  }

  throw new Error('Bridge timeout');
}
```

***

## EVM Contract Tests (Foundry)

### Running Tests

```bash
cd contracts/eth

# Run all tests
forge test

# Verbose output
forge test -vvvv

# Run specific test file
forge test --match-path test/MultiSigLocker.t.sol

# Run specific test function
forge test --match-test testLockTokens

# Gas report
forge test --gas-report

# Coverage
forge coverage
```

### Writing Tests

```solidity
// test/MultiSigLocker.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/MultiSigERC20Locker.sol";
import "../src/mocks/MockERC20.sol";

contract MultiSigLockerTest is Test {
    MultiSigERC20Locker public locker;
    MockERC20 public token;

    address public admin = address(1);
    address public user = address(2);
    address[] public relayers;
    uint256 public threshold = 3;

    function setUp() public {
        // Setup relayers
        for (uint i = 0; i < 5; i++) {
            relayers.push(address(uint160(100 + i)));
        }

        // Deploy contracts
        vm.prank(admin);
        locker = new MultiSigERC20Locker(relayers, threshold);

        token = new MockERC20("Test Token", "TEST", 6);

        // Fund user
        token.mint(user, 1000000 * 10**6);
    }

    function testLockTokens() public {
        uint256 amount = 100 * 10**6;
        bytes32 recipient = bytes32(uint256(0x01abc));

        vm.startPrank(user);

        // Approve
        token.approve(address(locker), amount);

        // Get fee
        uint256 fee = locker.bridgeFee();

        // Lock
        vm.deal(user, fee);
        locker.lock{value: fee}(address(token), amount, recipient);

        vm.stopPrank();

        // Verify
        assertEq(locker.getLockedBalance(address(token)), amount);
    }

    function testLockEmitsEvent() public {
        uint256 amount = 100 * 10**6;
        bytes32 recipient = bytes32(uint256(0x01abc));

        vm.startPrank(user);
        token.approve(address(locker), amount);
        uint256 fee = locker.bridgeFee();
        vm.deal(user, fee);

        vm.expectEmit(true, true, true, true);
        emit MultiSigERC20Locker.Lock(
            address(token),
            user,
            recipient,
            amount,
            1  // nonce
        );

        locker.lock{value: fee}(address(token), amount, recipient);
        vm.stopPrank();
    }

    function testUnlockWithSignatures() public {
        // Setup: Lock tokens first
        uint256 amount = 100 * 10**6;
        bytes32 recipient = bytes32(uint256(uint160(user)));
        vm.prank(admin);
        token.mint(address(locker), amount);

        // Create unlock attestation
        bytes32 eventKey = keccak256("test_event");
        bytes32 messageHash = locker.computeUnlockHash(
            address(token),
            user,
            amount,
            eventKey
        );

        // Collect signatures
        bytes[] memory signatures = new bytes[](3);
        for (uint i = 0; i < 3; i++) {
            (uint8 v, bytes32 r, bytes32 s) = vm.sign(
                uint256(uint160(relayers[i])),
                messageHash
            );
            signatures[i] = abi.encodePacked(r, s, v);
        }

        // Execute unlock
        uint256 balanceBefore = token.balanceOf(user);

        locker.unlock(
            address(token),
            user,
            amount,
            eventKey,
            signatures
        );

        assertEq(token.balanceOf(user), balanceBefore + amount);
    }

    function testRejectInsufficientSignatures() public {
        bytes32 eventKey = keccak256("test_event");
        bytes[] memory signatures = new bytes[](2); // Only 2, need 3

        vm.expectRevert("Insufficient signatures");
        locker.unlock(
            address(token),
            user,
            100 * 10**6,
            eventKey,
            signatures
        );
    }

    function testFuzzLockAmount(uint256 amount) public {
        vm.assume(amount > 0 && amount <= 1000000 * 10**6);

        bytes32 recipient = bytes32(uint256(0x01abc));

        vm.startPrank(user);
        token.approve(address(locker), amount);
        uint256 fee = locker.bridgeFee();
        vm.deal(user, fee);

        locker.lock{value: fee}(address(token), amount, recipient);
        vm.stopPrank();

        assertEq(locker.getLockedBalance(address(token)), amount);
    }
}
```

### Fork Testing

Test against live contracts:

```solidity
contract ForkTest is Test {
    function setUp() public {
        // Fork Sepolia at specific block
        vm.createSelectFork(
            vm.envString("SEPOLIA_RPC_URL"),
            5000000
        );
    }

    function testLiveContract() public {
        MultiSigERC20Locker locker = MultiSigERC20Locker(
            0x6aaea12cF566C9fddbC7082c15d38F1447a57D10
        );

        assertGt(locker.getRelayerCount(), 0);
    }
}
```

***

## Casper Contract Tests (Odra)

### Running Tests

```bash
cd contracts/casper

# Run all tests
cargo odra test

# Verbose output
cargo odra test -- --nocapture

# Specific test
cargo odra test test_mint_tokens

# With logs
RUST_LOG=debug cargo odra test
```

### Writing Tests

```rust
// src/tests/token_factory_tests.rs
#[cfg(test)]
mod tests {
    use super::*;
    use odra::host::{Deployer, HostRef};
    use odra::casper_types::U256;

    #[test]
    fn test_mint_tokens() {
        // Deploy contract
        let env = odra::test_env();
        let admin = env.get_account(0);

        let mut token_factory = TokenFactoryDeployer::deploy(
            &env,
            admin,
            vec![],  // relayers
            3,       // threshold
        );

        // Setup: Register token
        let chain_id = 11155111u64;
        let evm_token = [0u8; 20];
        let cep18_hash = [0u8; 32];

        token_factory.register_token_mapping(chain_id, evm_token, cep18_hash);

        // Create attestation
        let attestation = LockAttestation {
            source_chain_id: chain_id,
            evm_block_number: 1000000,
            evm_tx_index: 0,
            evm_log_index: 0,
            token: evm_token,
            sender: [0u8; 20],
            casper_recipient: admin.into(),
            amount: U256::from(100_000_000),
            nonce: 1,
            receipts_root: [0u8; 32],
        };

        // Collect signatures (in real test, use proper signing)
        let signatures = create_test_signatures(&attestation);

        // Execute mint
        token_factory.mint_with_signatures(attestation, signatures);

        // Verify balance
        let balance = token_factory.get_balance(cep18_hash, admin);
        assert_eq!(balance, U256::from(100_000_000));
    }

    #[test]
    fn test_burn_tokens() {
        let env = odra::test_env();
        let user = env.get_account(1);

        // Setup: Mint tokens first
        let mut token_factory = deploy_and_mint(&env);

        // Burn tokens
        let amount = U256::from(50_000_000);
        let recipient = [0xab; 20];  // EVM address
        let chain_id = 11155111u64;

        env.set_caller(user);
        let nonce = token_factory.burn(chain_id, recipient, amount);

        assert!(nonce > 0);
    }

    #[test]
    #[should_panic(expected = "Insufficient signatures")]
    fn test_reject_insufficient_signatures() {
        let env = odra::test_env();

        let mut token_factory = TokenFactoryDeployer::deploy(
            &env,
            env.get_account(0),
            vec![],
            3,  // Require 3 signatures
        );

        let attestation = create_test_attestation();
        let signatures = vec![vec![0u8; 65], vec![0u8; 65]];  // Only 2

        token_factory.mint_with_signatures(attestation, signatures);
    }

    #[test]
    fn test_replay_protection() {
        let env = odra::test_env();
        let mut token_factory = deploy_test_factory(&env);

        let attestation = create_test_attestation();
        let signatures = create_test_signatures(&attestation);

        // First mint should succeed
        token_factory.mint_with_signatures(attestation.clone(), signatures.clone());

        // Second mint with same attestation should fail
        let result = std::panic::catch_unwind(|| {
            token_factory.mint_with_signatures(attestation, signatures);
        });

        assert!(result.is_err());
    }
}
```

***

## Test Environment Variables

Create `cli/.env.test`:

```bash
# Test Networks
TEST_RPC_URL=http://localhost:8545
CASPER_TEST_NODE_URL=http://localhost:11101/rpc

# Test Keys (Anvil default)
TEST_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

# Test Contracts (deployed to Anvil)
TEST_LOCKER_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3
TEST_TOKEN_ADDRESS=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512

# API URL (if testing API)
API_URL=http://localhost:3000
```

> **Note**: Relayer tests use file-based persistence in a temporary `data/` directory. No database setup is required.

***

## CI/CD Testing

### GitHub Actions

```yaml
# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  typescript-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - uses: actions/setup-node@v3
        with:
          node-version: 18

      - name: Install dependencies
        run: cd cli && npm ci

      - name: Run tests
        run: cd cli && npm test

  solidity-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: foundry-rs/foundry-toolchain@v1

      - name: Run tests
        run: cd contracts/eth && forge test

  rust-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          target: wasm32-unknown-unknown

      - name: Run tests
        run: cd contracts/casper && cargo odra test
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.astralbeam.io/for-developers/testing.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
