Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions config/networks/sandbox.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# =============================================================
# TeachLink Sandbox Network Configuration
# Issue #381 β€” Local testing sandbox environment
# =============================================================
# This config is for LOCAL development only.
# Never use real keys or mainnet values here.

STELLAR_NETWORK=sandbox
STELLAR_HORIZON_URL=http://localhost:8000
STELLAR_SOROBAN_RPC_URL=http://localhost:8000/soroban/rpc

# Pre-funded sandbox deployer key (safe to commit - sandbox only)
DEPLOYER_SECRET_KEY=SCZANGBA5YHTNYVS23C4QSQH5ODHVIMTQJZNFNLXFMG7VZ57SB42ONHU

# Pre-funded sandbox test account keys
TEST_ACCOUNT_1_SECRET=SDHOAMBNLGCE27Q64SHG6KBSSRMLV3QLVJSRMGM65JJXTVFVKGAL4LGS
TEST_ACCOUNT_2_SECRET=SBQPDFUGLMWJYEYXFRM5TQX3AX2BR47WKI8FDS2VAKZ4YKQZRP64FGP5
TEST_ACCOUNT_3_SECRET=SAVDOZS4FVSYLBCM7YIMHZUZSZSZEZM7HHQMHW6WIZ3QEG7A7BXQZAAH

# Sandbox timing (faster than real network)
SANDBOX_BLOCK_TIME_MS=500
SANDBOX_TX_TIMEOUT_SECS=10

# Mock service ports
MOCK_HORIZON_PORT=8000
MOCK_TOKEN_PORT=8001
SANDBOX_RPC_PORT=8002
EOF
81 changes: 56 additions & 25 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,62 @@
version: '3.8'

services:
# Development environment with all tools
# ─────────────────────────────────────────────
# LOCAL STELLAR NODE (sandbox network)
# Issue #381 β€” provides isolated local blockchain
# ─────────────────────────────────────────────
stellar-local:
image: stellar/quickstart:latest
container_name: teachlink-stellar-local
ports:
- "8000:8000" # Horizon API
- "8001:8001" # Friendbot (free test XLM)
environment:
- ENABLE_SOROBAN_RPC=true
command: --local --enable-soroban-rpc
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 5s
timeout: 3s
retries: 20
networks:
- teachlink-network

# ─────────────────────────────────────────────
# SANDBOX β€” full isolated test environment
# Run: docker-compose up sandbox
# ─────────────────────────────────────────────
sandbox:
build:
context: .
target: development
dockerfile: Dockerfile
container_name: teachlink-sandbox
depends_on:
stellar-local:
condition: service_healthy
volumes:
- .:/workspace
- cargo-cache:/usr/local/cargo/registry
- target-cache:/workspace/target
environment:
- RUST_BACKTRACE=1
- CARGO_TERM_COLOR=always
- STELLAR_NETWORK=sandbox
- STELLAR_HORIZON_URL=http://stellar-local:8000
- STELLAR_SOROBAN_RPC_URL=http://stellar-local:8000/soroban/rpc
- DEPLOYER_SECRET_KEY=SCZANGBA5YHTNYVS23C4QSQH5ODHVIMTQJZNFNLXFMG7VZ57SB42ONHU
- TEST_ACCOUNT_1_SECRET=SDHOAMBNLGCE27Q64SHG6KBSSRMLV3QLVJSRMGM65JJXTVFVKGAL4LGS
- TEST_ACCOUNT_2_SECRET=SBQPDFUGLMWJYEYXFRM5TQX3AX2BR47WKI8FDS2VAKZ4YKQZRP64FGP5
- SANDBOX_BLOCK_TIME_MS=500
working_dir: /workspace
command: cargo test --all-features -- --test-threads=1
networks:
- teachlink-network

# ─────────────────────────────────────────────
# EXISTING SERVICES (unchanged)
# ─────────────────────────────────────────────
dev:
build:
context: .
Expand All @@ -24,7 +79,6 @@ services:
networks:
- teachlink-network

# Builder service for creating optimized WASM
builder:
build:
context: .
Expand All @@ -39,7 +93,6 @@ services:
networks:
- teachlink-network

# Test runner service
test:
build:
context: .
Expand All @@ -55,7 +108,6 @@ services:
networks:
- teachlink-network

# Linter/formatter service
lint:
build:
context: .
Expand All @@ -79,24 +131,3 @@ volumes:
networks:
teachlink-network:
driver: bridge

# Usage instructions:
#
# Start development environment:
# docker-compose up dev
# docker-compose exec dev bash
#
# Build WASM:
# docker-compose run --rm builder
#
# Run tests:
# docker-compose run --rm test
#
# Run linter:
# docker-compose run --rm lint
#
# Build all services:
# docker-compose build
#
# Clean up:
# docker-compose down -v
98 changes: 98 additions & 0 deletions testing/sandbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@

# TeachLink Testing Sandbox

> Issue #381 β€” Comprehensive local testing environment

The sandbox gives every developer a fully isolated, reproducible
environment to write and run tests β€” no testnet, no real keys, no
waiting for network confirmations.

---

## Quick Start

```bash
# One command β€” starts everything, runs tests, cleans up
./scripts/sandbox.sh

# Run tests only (no Docker required)
./scripts/sandbox.sh --no-docker

# Keep the local node running after tests (for manual exploration)
./scripts/sandbox.sh --keep-up
```

---

## What the Sandbox Provides

| Feature | Description |
|---|---|
| Local Stellar node | Full Soroban-enabled node, no testnet needed |
| Mock token | SEP-41 compatible token β€” mint freely in tests |
| Named test accounts | `alice`, `bob`, `carol`, `dave` β€” readable tests |
| Time control | `advance_time()` and `advance_ledger()` helpers |
| Isolated environment | Every test gets a clean slate |
| Fast iteration | No network delay β€” tests run in milliseconds |

---

## Using the Sandbox in Your Tests

```rust
use crate::testing::sandbox::SandboxEnv;
use crate::testing::sandbox::fixtures::amounts;

#[test]
fn test_reward_distribution() {
// 1. Create a fresh sandbox
let sb = SandboxEnv::new();

// 2. Use named accounts
let educator = sb.accounts.bob();
let learner = sb.accounts.alice();

// 3. Use amount constants
let reward = amounts::TEN_XLM;

// 4. Advance time if your contract needs it
sb.advance_time(fixtures::time::ONE_DAY);

// 5. Write assertions normally
assert!(reward > 0);
}
```

---

## Using the Mock Token

```rust
use crate::testing::sandbox::{SandboxEnv, mock_token::MockToken};

#[test]
fn test_escrow_with_token() {
let sb = SandboxEnv::new();

// Deploy the mock token
let token_id = sb.env.register_contract(None, MockToken);
let token = MockTokenClient::new(&sb.env, &token_id);

// Initialize it
token.initialize(
&sb.accounts.carol(), // carol = admin
&7u32,
&String::from_str(&sb.env, "TeachLink Token"),
&String::from_str(&sb.env, "TLT"),
);

// Mint tokens freely β€” no real XLM needed
token.mint(&sb.accounts.carol(), &sb.accounts.alice(), &1_000_0000000i128);

assert_eq!(token.balance(&sb.accounts.alice()), 1_000_0000000i128);
}
```

---

## File Structure
93 changes: 93 additions & 0 deletions testing/sandbox/fixtures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@

//! # Test Fixtures
//! Issue #381 β€” Pre-built test accounts and data helpers
//!
//! Named accounts make tests readable:
//! alice = typical learner
//! bob = typical educator
//! carol = platform admin / third party
//! dave = adversarial / edge-case actor

use soroban_sdk::{Address, Env};

/// Named test accounts for readable, expressive tests.
pub struct TestAccounts {
alice: Address,
bob: Address,
carol: Address,
dave: Address,
}

impl TestAccounts {
/// Create all named accounts bound to the given environment.
pub fn new(env: &Env) -> Self {
Self {
alice: Address::generate(env),
bob: Address::generate(env),
carol: Address::generate(env),
dave: Address::generate(env),
}
}

/// Alice β€” typical learner account
pub fn alice(&self) -> Address { self.alice.clone() }

/// Bob β€” typical educator account
pub fn bob(&self) -> Address { self.bob.clone() }

/// Carol β€” platform admin or neutral third party
pub fn carol(&self) -> Address { self.carol.clone() }

/// Dave β€” adversarial or edge-case actor
pub fn dave(&self) -> Address { self.dave.clone() }
}

/// Standard token amounts used across tests (in stroops, 1 XLM = 10_000_000)
pub mod amounts {
pub const ONE_XLM: i128 = 10_000_000;
pub const TEN_XLM: i128 = 100_000_000;
pub const HUNDRED_XLM: i128 = 1_000_000_000;
pub const THOUSAND_XLM: i128 = 10_000_000_000;
pub const PLATFORM_FEE_BPS: i128 = 250; // 2.5%
}

/// Standard time values used across tests (in seconds)
pub mod time {
pub const ONE_MINUTE: u64 = 60;
pub const ONE_HOUR: u64 = 3_600;
pub const ONE_DAY: u64 = 86_400;
pub const ONE_WEEK: u64 = 604_800;
pub const ONE_MONTH: u64 = 2_592_000;
}

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::Env;

#[test]
fn all_accounts_generated() {
let env = Env::default();
let accounts = TestAccounts::new(&env);
// All four accounts must be distinct addresses
let all = [
accounts.alice(),
accounts.bob(),
accounts.carol(),
accounts.dave(),
];
for i in 0..all.len() {
for j in (i + 1)..all.len() {
assert_ne!(all[i], all[j], "accounts[{i}] == accounts[{j}] β€” must be unique");
}
}
}

#[test]
fn amount_constants_are_correct() {
use amounts::*;
assert_eq!(ONE_XLM * 10, TEN_XLM);
assert_eq!(TEN_XLM * 10, HUNDRED_XLM);
assert_eq!(HUNDRED_XLM * 10, THOUSAND_XLM);
}
}
Loading
Loading