x402-engineer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT.md +102 -0
- package/README.md +43 -0
- package/dist/cli.cjs +137 -0
- package/package.json +51 -0
- package/skills/stellar-dev/SKILL.md +146 -0
- package/skills/stellar-dev/advanced-patterns.md +188 -0
- package/skills/stellar-dev/api-rpc-horizon.md +521 -0
- package/skills/stellar-dev/common-pitfalls.md +510 -0
- package/skills/stellar-dev/contracts-soroban.md +565 -0
- package/skills/stellar-dev/ecosystem.md +430 -0
- package/skills/stellar-dev/frontend-stellar-sdk.md +651 -0
- package/skills/stellar-dev/resources.md +306 -0
- package/skills/stellar-dev/security.md +491 -0
- package/skills/stellar-dev/standards-reference.md +94 -0
- package/skills/stellar-dev/stellar-assets.md +419 -0
- package/skills/stellar-dev/testing.md +786 -0
- package/skills/stellar-dev/zk-proofs.md +136 -0
- package/skills/x402-add-paywall/SKILL.md +208 -0
- package/skills/x402-add-paywall/references/patterns.md +132 -0
- package/skills/x402-debug/SKILL.md +92 -0
- package/skills/x402-debug/references/checklist.md +146 -0
- package/skills/x402-explain/SKILL.md +136 -0
- package/skills/x402-init/SKILL.md +129 -0
- package/skills/x402-init/templates/env-example.md +17 -0
- package/skills/x402-init/templates/express/config.ts.md +29 -0
- package/skills/x402-init/templates/express/server.ts.md +30 -0
- package/skills/x402-init/templates/fastify/adapter.ts.md +66 -0
- package/skills/x402-init/templates/fastify/config.ts.md +29 -0
- package/skills/x402-init/templates/fastify/server.ts.md +90 -0
- package/skills/x402-init/templates/hono/config.ts.md +29 -0
- package/skills/x402-init/templates/hono/server.ts.md +31 -0
- package/skills/x402-init/templates/next-app-router/config.ts.md +29 -0
- package/skills/x402-init/templates/next-app-router/server.ts.md +31 -0
- package/skills/x402-stellar/SKILL.md +139 -0
- package/skills/x402-stellar/references/api.md +237 -0
- package/skills/x402-stellar/references/patterns.md +276 -0
- package/skills/x402-stellar/references/setup.md +138 -0
- package/skills/x402-stellar/scripts/check-deps.js +218 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
# Testing Strategy (Local / Testnet / Unit Tests)
|
|
2
|
+
|
|
3
|
+
## Quick Navigation
|
|
4
|
+
- Strategy overview: [Testing Pyramid](#testing-pyramid)
|
|
5
|
+
- Core test layers: [Unit Testing with Soroban SDK](#unit-testing-with-soroban-sdk), [Local Testing with Stellar Quickstart](#local-testing-with-stellar-quickstart), [Testnet Testing](#testnet-testing)
|
|
6
|
+
- Integration and CI: [Integration Testing Patterns](#integration-testing-patterns), [Test Configuration](#test-configuration), [CI/CD Configuration](#cicd-configuration)
|
|
7
|
+
- Advanced testing: [Fuzz Testing](#fuzz-testing), [Property-Based Testing](#property-based-testing), [Differential Testing with Test Snapshots](#differential-testing-with-test-snapshots), [Fork Testing](#fork-testing), [Mutation Testing](#mutation-testing)
|
|
8
|
+
- Performance and readiness: [Resource Profiling](#resource-profiling), [Best Practices](#best-practices)
|
|
9
|
+
|
|
10
|
+
## Testing Pyramid
|
|
11
|
+
|
|
12
|
+
1. **Unit tests (fast)**: Native Rust tests with `soroban-sdk` testutils
|
|
13
|
+
2. **Local integration tests**: Stellar Quickstart Docker
|
|
14
|
+
3. **Testnet tests**: Deploy and test on public testnet
|
|
15
|
+
4. **Mainnet smoke tests**: Final validation before production
|
|
16
|
+
|
|
17
|
+
## Unit Testing with Soroban SDK
|
|
18
|
+
|
|
19
|
+
The Soroban SDK provides comprehensive testing utilities that run natively (not in WASM), enabling fast iteration with full debugging support.
|
|
20
|
+
|
|
21
|
+
### Basic Test Setup
|
|
22
|
+
|
|
23
|
+
```rust
|
|
24
|
+
#![cfg(test)]
|
|
25
|
+
|
|
26
|
+
use soroban_sdk::{testutils::Address as _, Address, Env};
|
|
27
|
+
|
|
28
|
+
// Import your contract
|
|
29
|
+
use crate::{Contract, ContractClient};
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn test_basic_functionality() {
|
|
33
|
+
// Create test environment
|
|
34
|
+
let env = Env::default();
|
|
35
|
+
|
|
36
|
+
// Register contract
|
|
37
|
+
let contract_id = env.register_contract(None, Contract);
|
|
38
|
+
|
|
39
|
+
// Create typed client
|
|
40
|
+
let client = ContractClient::new(&env, &contract_id);
|
|
41
|
+
|
|
42
|
+
// Generate test addresses
|
|
43
|
+
let user = Address::generate(&env);
|
|
44
|
+
|
|
45
|
+
// Call contract functions
|
|
46
|
+
client.initialize(&user);
|
|
47
|
+
|
|
48
|
+
// Assert results
|
|
49
|
+
assert_eq!(client.get_value(), 0);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Testing Authorization
|
|
54
|
+
|
|
55
|
+
```rust
|
|
56
|
+
#[test]
|
|
57
|
+
fn test_with_auth() {
|
|
58
|
+
let env = Env::default();
|
|
59
|
+
|
|
60
|
+
// Mock all authorizations automatically
|
|
61
|
+
env.mock_all_auths();
|
|
62
|
+
|
|
63
|
+
let contract_id = env.register_contract(None, TokenContract);
|
|
64
|
+
let client = TokenContractClient::new(&env, &contract_id);
|
|
65
|
+
|
|
66
|
+
let admin = Address::generate(&env);
|
|
67
|
+
let user1 = Address::generate(&env);
|
|
68
|
+
let user2 = Address::generate(&env);
|
|
69
|
+
|
|
70
|
+
// Initialize and mint
|
|
71
|
+
client.initialize(&admin);
|
|
72
|
+
client.mint(&user1, &1000);
|
|
73
|
+
|
|
74
|
+
// Transfer (requires auth from user1)
|
|
75
|
+
client.transfer(&user1, &user2, &100);
|
|
76
|
+
|
|
77
|
+
assert_eq!(client.balance(&user1), 900);
|
|
78
|
+
assert_eq!(client.balance(&user2), 100);
|
|
79
|
+
|
|
80
|
+
// Verify which auths were required
|
|
81
|
+
let auths = env.auths();
|
|
82
|
+
assert_eq!(auths.len(), 1);
|
|
83
|
+
// auths[0] contains (address, contract_id, function, args)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Testing with Specific Auth Requirements
|
|
88
|
+
|
|
89
|
+
```rust
|
|
90
|
+
#[test]
|
|
91
|
+
fn test_specific_auth() {
|
|
92
|
+
let env = Env::default();
|
|
93
|
+
let contract_id = env.register_contract(None, Contract);
|
|
94
|
+
let client = ContractClient::new(&env, &contract_id);
|
|
95
|
+
|
|
96
|
+
let user = Address::generate(&env);
|
|
97
|
+
|
|
98
|
+
// Mock auth only for specific address
|
|
99
|
+
env.mock_auths(&[MockAuth {
|
|
100
|
+
address: &user,
|
|
101
|
+
invoke: &MockAuthInvoke {
|
|
102
|
+
contract: &contract_id,
|
|
103
|
+
fn_name: "transfer",
|
|
104
|
+
args: (&user, &other, &100i128).into_val(&env),
|
|
105
|
+
sub_invokes: &[],
|
|
106
|
+
},
|
|
107
|
+
}]);
|
|
108
|
+
|
|
109
|
+
client.transfer(&user, &other, &100);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Testing Time-Dependent Logic
|
|
114
|
+
|
|
115
|
+
```rust
|
|
116
|
+
#[test]
|
|
117
|
+
fn test_time_based() {
|
|
118
|
+
let env = Env::default();
|
|
119
|
+
let contract_id = env.register_contract(None, VestingContract);
|
|
120
|
+
let client = VestingContractClient::new(&env, &contract_id);
|
|
121
|
+
|
|
122
|
+
let beneficiary = Address::generate(&env);
|
|
123
|
+
|
|
124
|
+
// Set initial timestamp
|
|
125
|
+
env.ledger().set_timestamp(1000);
|
|
126
|
+
|
|
127
|
+
client.create_vesting(&beneficiary, &1000, &2000); // unlock at t=2000
|
|
128
|
+
|
|
129
|
+
// Try to claim before unlock
|
|
130
|
+
assert!(client.try_claim(&beneficiary).is_err());
|
|
131
|
+
|
|
132
|
+
// Advance time past unlock
|
|
133
|
+
env.ledger().set_timestamp(2500);
|
|
134
|
+
|
|
135
|
+
// Now claim succeeds
|
|
136
|
+
client.claim(&beneficiary);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Testing Ledger State
|
|
141
|
+
|
|
142
|
+
```rust
|
|
143
|
+
#[test]
|
|
144
|
+
fn test_ledger_manipulation() {
|
|
145
|
+
let env = Env::default();
|
|
146
|
+
|
|
147
|
+
// Set ledger sequence
|
|
148
|
+
env.ledger().set_sequence_number(1000);
|
|
149
|
+
|
|
150
|
+
// Set timestamp
|
|
151
|
+
env.ledger().set_timestamp(1704067200); // Jan 1, 2024
|
|
152
|
+
|
|
153
|
+
// Set network passphrase
|
|
154
|
+
env.ledger().set_network_id([0u8; 32]); // Custom network ID
|
|
155
|
+
|
|
156
|
+
// Get current values
|
|
157
|
+
let seq = env.ledger().sequence();
|
|
158
|
+
let ts = env.ledger().timestamp();
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Testing Events
|
|
163
|
+
|
|
164
|
+
```rust
|
|
165
|
+
#[test]
|
|
166
|
+
fn test_events() {
|
|
167
|
+
let env = Env::default();
|
|
168
|
+
let contract_id = env.register_contract(None, Contract);
|
|
169
|
+
let client = ContractClient::new(&env, &contract_id);
|
|
170
|
+
|
|
171
|
+
client.do_something();
|
|
172
|
+
|
|
173
|
+
// Get all events
|
|
174
|
+
let events = env.events().all();
|
|
175
|
+
|
|
176
|
+
// Check specific event
|
|
177
|
+
assert_eq!(events.len(), 1);
|
|
178
|
+
|
|
179
|
+
let event = &events[0];
|
|
180
|
+
// event.0 = contract_id
|
|
181
|
+
// event.1 = topics (Vec<Val>)
|
|
182
|
+
// event.2 = data (Val)
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Testing Storage
|
|
187
|
+
|
|
188
|
+
```rust
|
|
189
|
+
#[test]
|
|
190
|
+
fn test_storage_ttl() {
|
|
191
|
+
let env = Env::default();
|
|
192
|
+
let contract_id = env.register_contract(None, Contract);
|
|
193
|
+
let client = ContractClient::new(&env, &contract_id);
|
|
194
|
+
|
|
195
|
+
client.store_data();
|
|
196
|
+
|
|
197
|
+
// Check TTL
|
|
198
|
+
let key = DataKey::MyData;
|
|
199
|
+
let ttl = env.as_contract(&contract_id, || {
|
|
200
|
+
env.storage().persistent().get_ttl(&key)
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
assert!(ttl > 0);
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Testing Cross-Contract Calls
|
|
208
|
+
|
|
209
|
+
```rust
|
|
210
|
+
#[test]
|
|
211
|
+
fn test_cross_contract() {
|
|
212
|
+
let env = Env::default();
|
|
213
|
+
|
|
214
|
+
// Register both contracts
|
|
215
|
+
let token_id = env.register_contract_wasm(None, token::WASM);
|
|
216
|
+
let vault_id = env.register_contract(None, VaultContract);
|
|
217
|
+
|
|
218
|
+
let token_client = token::Client::new(&env, &token_id);
|
|
219
|
+
let vault_client = VaultContractClient::new(&env, &vault_id);
|
|
220
|
+
|
|
221
|
+
env.mock_all_auths();
|
|
222
|
+
|
|
223
|
+
let user = Address::generate(&env);
|
|
224
|
+
|
|
225
|
+
// Setup: mint tokens to user
|
|
226
|
+
token_client.mint(&user, &1000);
|
|
227
|
+
|
|
228
|
+
// Test: deposit tokens into vault
|
|
229
|
+
vault_client.deposit(&user, &token_id, &500);
|
|
230
|
+
|
|
231
|
+
assert_eq!(token_client.balance(&user), 500);
|
|
232
|
+
assert_eq!(vault_client.balance(&user), 500);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Local Testing with Stellar Quickstart
|
|
237
|
+
|
|
238
|
+
### Start Local Network
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# Pull and run Stellar Quickstart
|
|
242
|
+
docker run --rm -it -p 8000:8000 \
|
|
243
|
+
--name stellar \
|
|
244
|
+
stellar/quickstart:latest \
|
|
245
|
+
--local \
|
|
246
|
+
--enable-soroban-rpc
|
|
247
|
+
|
|
248
|
+
# Or use Stellar CLI
|
|
249
|
+
stellar container start local
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Configure for Local Network
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import * as StellarSdk from "@stellar/stellar-sdk";
|
|
256
|
+
|
|
257
|
+
const LOCAL_RPC = "http://localhost:8000/soroban/rpc";
|
|
258
|
+
const LOCAL_HORIZON = "http://localhost:8000";
|
|
259
|
+
const LOCAL_PASSPHRASE = "Standalone Network ; February 2017";
|
|
260
|
+
|
|
261
|
+
const rpc = new StellarSdk.rpc.Server(LOCAL_RPC);
|
|
262
|
+
const horizon = new StellarSdk.Horizon.Server(LOCAL_HORIZON);
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Fund Test Accounts (Local)
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
# Using Stellar CLI
|
|
269
|
+
stellar keys generate --global test-account --network local --fund
|
|
270
|
+
|
|
271
|
+
# Or via friendbot endpoint
|
|
272
|
+
curl "http://localhost:8000/friendbot?addr=G..."
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Deploy and Test Locally
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# Deploy contract to local network
|
|
279
|
+
stellar contract deploy \
|
|
280
|
+
--wasm target/wasm32-unknown-unknown/release/contract.wasm \
|
|
281
|
+
--source test-account \
|
|
282
|
+
--network local
|
|
283
|
+
|
|
284
|
+
# Invoke contract
|
|
285
|
+
stellar contract invoke \
|
|
286
|
+
--id CONTRACT_ID \
|
|
287
|
+
--source test-account \
|
|
288
|
+
--network local \
|
|
289
|
+
-- \
|
|
290
|
+
function_name \
|
|
291
|
+
--arg value
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Testnet Testing
|
|
295
|
+
|
|
296
|
+
### Network Configuration
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
# Testnet RPC: https://soroban-testnet.stellar.org
|
|
300
|
+
# Testnet Horizon: https://horizon-testnet.stellar.org
|
|
301
|
+
# Network Passphrase: "Test SDF Network ; September 2015"
|
|
302
|
+
# Friendbot: https://friendbot.stellar.org
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Create and Fund Testnet Account
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
# Generate new identity
|
|
309
|
+
stellar keys generate --global my-testnet-key --network testnet
|
|
310
|
+
|
|
311
|
+
# Fund via Friendbot
|
|
312
|
+
stellar keys fund my-testnet-key --network testnet
|
|
313
|
+
|
|
314
|
+
# Or manually
|
|
315
|
+
curl "https://friendbot.stellar.org?addr=G..."
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Deploy to Testnet
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# Deploy contract
|
|
322
|
+
stellar contract deploy \
|
|
323
|
+
--wasm target/wasm32-unknown-unknown/release/contract.wasm \
|
|
324
|
+
--source my-testnet-key \
|
|
325
|
+
--network testnet
|
|
326
|
+
|
|
327
|
+
# Install contract code (separate from deployment)
|
|
328
|
+
stellar contract install \
|
|
329
|
+
--wasm target/wasm32-unknown-unknown/release/contract.wasm \
|
|
330
|
+
--source my-testnet-key \
|
|
331
|
+
--network testnet
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Testnet Reset Awareness
|
|
335
|
+
|
|
336
|
+
**Important**: Testnet resets approximately quarterly:
|
|
337
|
+
- All accounts and contracts are deleted
|
|
338
|
+
- Plan for re-deployment after resets
|
|
339
|
+
- Don't rely on persistent state for test data
|
|
340
|
+
|
|
341
|
+
Check reset schedule: https://stellar.org/developers/blog
|
|
342
|
+
|
|
343
|
+
## Integration Testing Patterns
|
|
344
|
+
|
|
345
|
+
### TypeScript Integration Tests
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// tests/integration/contract.test.ts
|
|
349
|
+
import * as StellarSdk from "@stellar/stellar-sdk";
|
|
350
|
+
|
|
351
|
+
const RPC_URL = process.env.RPC_URL || "http://localhost:8000/soroban/rpc";
|
|
352
|
+
const NETWORK_PASSPHRASE = process.env.NETWORK_PASSPHRASE || "Standalone Network ; February 2017";
|
|
353
|
+
|
|
354
|
+
describe("Contract Integration Tests", () => {
|
|
355
|
+
let rpc: StellarSdk.rpc.Server;
|
|
356
|
+
let keypair: StellarSdk.Keypair;
|
|
357
|
+
let contractId: string;
|
|
358
|
+
|
|
359
|
+
beforeAll(async () => {
|
|
360
|
+
rpc = new StellarSdk.rpc.Server(RPC_URL);
|
|
361
|
+
keypair = StellarSdk.Keypair.random();
|
|
362
|
+
|
|
363
|
+
// Fund account
|
|
364
|
+
await fundAccount(keypair.publicKey());
|
|
365
|
+
|
|
366
|
+
// Deploy contract
|
|
367
|
+
contractId = await deployContract(keypair);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("should initialize contract", async () => {
|
|
371
|
+
const account = await rpc.getAccount(keypair.publicKey());
|
|
372
|
+
const contract = new StellarSdk.Contract(contractId);
|
|
373
|
+
|
|
374
|
+
const tx = new StellarSdk.TransactionBuilder(account, {
|
|
375
|
+
fee: "100",
|
|
376
|
+
networkPassphrase: NETWORK_PASSPHRASE,
|
|
377
|
+
})
|
|
378
|
+
.addOperation(
|
|
379
|
+
contract.call(
|
|
380
|
+
"initialize",
|
|
381
|
+
StellarSdk.Address.fromString(keypair.publicKey()).toScVal()
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
.setTimeout(30)
|
|
385
|
+
.build();
|
|
386
|
+
|
|
387
|
+
const simResult = await rpc.simulateTransaction(tx);
|
|
388
|
+
const preparedTx = StellarSdk.rpc.assembleTransaction(tx, simResult);
|
|
389
|
+
|
|
390
|
+
preparedTx.sign(keypair);
|
|
391
|
+
const result = await rpc.sendTransaction(preparedTx.build());
|
|
392
|
+
|
|
393
|
+
expect(result.status).not.toBe("ERROR");
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Rust Integration Tests
|
|
399
|
+
|
|
400
|
+
```rust
|
|
401
|
+
// tests/integration_test.rs
|
|
402
|
+
use soroban_sdk::{Env, Address};
|
|
403
|
+
use std::process::Command;
|
|
404
|
+
|
|
405
|
+
#[test]
|
|
406
|
+
#[ignore] // Run with: cargo test -- --ignored
|
|
407
|
+
fn integration_test_with_local_network() {
|
|
408
|
+
// Requires local network running
|
|
409
|
+
let output = Command::new("stellar")
|
|
410
|
+
.args([
|
|
411
|
+
"contract", "invoke",
|
|
412
|
+
"--id", "CONTRACT_ID",
|
|
413
|
+
"--source", "test-account",
|
|
414
|
+
"--network", "local",
|
|
415
|
+
"--",
|
|
416
|
+
"get_count"
|
|
417
|
+
])
|
|
418
|
+
.output()
|
|
419
|
+
.expect("Failed to invoke contract");
|
|
420
|
+
|
|
421
|
+
assert!(output.status.success());
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Test Configuration
|
|
426
|
+
|
|
427
|
+
### Cargo.toml for Tests
|
|
428
|
+
|
|
429
|
+
```toml
|
|
430
|
+
[dev-dependencies]
|
|
431
|
+
soroban-sdk = { version = "25.0.1", features = ["testutils"] } # match [dependencies] version
|
|
432
|
+
|
|
433
|
+
[profile.test]
|
|
434
|
+
opt-level = 0
|
|
435
|
+
debug = true
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Running Tests
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
# Run unit tests
|
|
442
|
+
cargo test
|
|
443
|
+
|
|
444
|
+
# Run with output
|
|
445
|
+
cargo test -- --nocapture
|
|
446
|
+
|
|
447
|
+
# Run specific test
|
|
448
|
+
cargo test test_transfer
|
|
449
|
+
|
|
450
|
+
# Run ignored (integration) tests
|
|
451
|
+
cargo test -- --ignored
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## CI/CD Configuration
|
|
455
|
+
|
|
456
|
+
### GitHub Actions Example
|
|
457
|
+
|
|
458
|
+
```yaml
|
|
459
|
+
name: Test Soroban Contract
|
|
460
|
+
|
|
461
|
+
on: [push, pull_request]
|
|
462
|
+
|
|
463
|
+
jobs:
|
|
464
|
+
unit-tests:
|
|
465
|
+
runs-on: ubuntu-latest
|
|
466
|
+
steps:
|
|
467
|
+
- uses: actions/checkout@v4
|
|
468
|
+
|
|
469
|
+
- name: Install Rust
|
|
470
|
+
uses: dtolnay/rust-toolchain@stable
|
|
471
|
+
|
|
472
|
+
- name: Add WASM target
|
|
473
|
+
run: rustup target add wasm32-unknown-unknown
|
|
474
|
+
|
|
475
|
+
- name: Run unit tests
|
|
476
|
+
run: cargo test
|
|
477
|
+
|
|
478
|
+
- name: Build contract
|
|
479
|
+
run: cargo build --release --target wasm32-unknown-unknown
|
|
480
|
+
|
|
481
|
+
integration-tests:
|
|
482
|
+
runs-on: ubuntu-latest
|
|
483
|
+
needs: unit-tests
|
|
484
|
+
services:
|
|
485
|
+
stellar:
|
|
486
|
+
image: stellar/quickstart:latest
|
|
487
|
+
ports:
|
|
488
|
+
- 8000:8000
|
|
489
|
+
options: >-
|
|
490
|
+
--health-cmd "curl -f http://localhost:8000 || exit 1"
|
|
491
|
+
--health-interval 10s
|
|
492
|
+
--health-timeout 5s
|
|
493
|
+
--health-retries 10
|
|
494
|
+
|
|
495
|
+
steps:
|
|
496
|
+
- uses: actions/checkout@v4
|
|
497
|
+
|
|
498
|
+
- name: Install Stellar CLI
|
|
499
|
+
run: |
|
|
500
|
+
cargo install stellar-cli --locked
|
|
501
|
+
|
|
502
|
+
- name: Deploy and test
|
|
503
|
+
run: |
|
|
504
|
+
stellar keys generate --global ci-test --network local --fund
|
|
505
|
+
stellar contract deploy \
|
|
506
|
+
--wasm target/wasm32-unknown-unknown/release/contract.wasm \
|
|
507
|
+
--source ci-test \
|
|
508
|
+
--network local
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## Best Practices
|
|
512
|
+
|
|
513
|
+
### Test Organization
|
|
514
|
+
```
|
|
515
|
+
project/
|
|
516
|
+
├── src/
|
|
517
|
+
│ └── lib.rs
|
|
518
|
+
├── tests/
|
|
519
|
+
│ ├── common/
|
|
520
|
+
│ │ └── mod.rs # Shared test utilities
|
|
521
|
+
│ ├── unit/
|
|
522
|
+
│ │ ├── mod.rs
|
|
523
|
+
│ │ └── transfer.rs
|
|
524
|
+
│ └── integration/
|
|
525
|
+
│ └── full_flow.rs
|
|
526
|
+
└── Cargo.toml
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Test Utilities Module
|
|
530
|
+
|
|
531
|
+
```rust
|
|
532
|
+
// tests/common/mod.rs
|
|
533
|
+
use soroban_sdk::{testutils::Address as _, Address, Env};
|
|
534
|
+
use crate::{Contract, ContractClient};
|
|
535
|
+
|
|
536
|
+
pub fn setup_contract(env: &Env) -> (Address, ContractClient) {
|
|
537
|
+
let contract_id = env.register_contract(None, Contract);
|
|
538
|
+
let client = ContractClient::new(env, &contract_id);
|
|
539
|
+
let admin = Address::generate(env);
|
|
540
|
+
|
|
541
|
+
env.mock_all_auths();
|
|
542
|
+
client.initialize(&admin);
|
|
543
|
+
|
|
544
|
+
(contract_id, client)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
pub fn create_funded_user(env: &Env, client: &ContractClient, amount: i128) -> Address {
|
|
548
|
+
let user = Address::generate(env);
|
|
549
|
+
client.mint(&user, &amount);
|
|
550
|
+
user
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
## Fuzz Testing
|
|
555
|
+
|
|
556
|
+
Soroban has first-class fuzz testing via `cargo-fuzz` and the built-in `SorobanArbitrary` trait. All `#[contracttype]` types automatically derive `SorobanArbitrary` when the `"testutils"` feature is active.
|
|
557
|
+
|
|
558
|
+
### Setup
|
|
559
|
+
|
|
560
|
+
```bash
|
|
561
|
+
# Install nightly Rust + cargo-fuzz
|
|
562
|
+
rustup install nightly
|
|
563
|
+
cargo install --locked cargo-fuzz
|
|
564
|
+
|
|
565
|
+
# Initialize fuzz targets
|
|
566
|
+
cargo fuzz init
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Update `Cargo.toml` to include both crate types:
|
|
570
|
+
```toml
|
|
571
|
+
[lib]
|
|
572
|
+
crate-type = ["lib", "cdylib"]
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Add to `fuzz/Cargo.toml`:
|
|
576
|
+
```toml
|
|
577
|
+
[dependencies]
|
|
578
|
+
soroban-sdk = { version = "25.0.1", features = ["testutils"] }
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Writing a Fuzz Target
|
|
582
|
+
|
|
583
|
+
```rust
|
|
584
|
+
// fuzz/fuzz_targets/fuzz_deposit.rs
|
|
585
|
+
#![no_main]
|
|
586
|
+
|
|
587
|
+
use libfuzzer_sys::fuzz_target;
|
|
588
|
+
use soroban_sdk::{testutils::Address as _, Address, Env};
|
|
589
|
+
use my_contract::{Contract, ContractClient};
|
|
590
|
+
|
|
591
|
+
fuzz_target!(|input: (u64, i128)| {
|
|
592
|
+
let (seed, amount) = input;
|
|
593
|
+
let env = Env::default();
|
|
594
|
+
env.mock_all_auths();
|
|
595
|
+
|
|
596
|
+
let contract_id = env.register(Contract, ());
|
|
597
|
+
let client = ContractClient::new(&env, &contract_id);
|
|
598
|
+
let user = Address::generate(&env);
|
|
599
|
+
|
|
600
|
+
// Initialize
|
|
601
|
+
client.initialize(&user);
|
|
602
|
+
|
|
603
|
+
// Fuzz deposit — should never panic unexpectedly
|
|
604
|
+
let _ = client.try_deposit(&user, &amount);
|
|
605
|
+
});
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Running Fuzz Tests
|
|
609
|
+
|
|
610
|
+
```bash
|
|
611
|
+
# Run (use --sanitizer=thread on macOS)
|
|
612
|
+
cargo +nightly fuzz run fuzz_deposit
|
|
613
|
+
|
|
614
|
+
# Generate code coverage
|
|
615
|
+
cargo +nightly fuzz coverage fuzz_deposit
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### Soroban Token Fuzzer
|
|
619
|
+
|
|
620
|
+
Reusable library for fuzzing token contracts:
|
|
621
|
+
- **GitHub**: https://github.com/brson/soroban-token-fuzzer
|
|
622
|
+
|
|
623
|
+
### Documentation
|
|
624
|
+
|
|
625
|
+
- [Stellar Fuzzing Guide](https://developers.stellar.org/docs/build/guides/testing/fuzzing)
|
|
626
|
+
- [Fuzzing Example Contract](https://developers.stellar.org/docs/build/smart-contracts/example-contracts/fuzzing)
|
|
627
|
+
|
|
628
|
+
## Property-Based Testing
|
|
629
|
+
|
|
630
|
+
Use `proptest` with `SorobanArbitrary` for QuickCheck-style property testing that runs in standard `cargo test`.
|
|
631
|
+
|
|
632
|
+
```rust
|
|
633
|
+
#[cfg(test)]
|
|
634
|
+
mod prop_tests {
|
|
635
|
+
use super::*;
|
|
636
|
+
use proptest::prelude::*;
|
|
637
|
+
use soroban_sdk::{testutils::Address as _, Env};
|
|
638
|
+
|
|
639
|
+
proptest! {
|
|
640
|
+
#[test]
|
|
641
|
+
fn deposit_then_withdraw_preserves_balance(amount in 1i128..=i128::MAX) {
|
|
642
|
+
let env = Env::default();
|
|
643
|
+
env.mock_all_auths();
|
|
644
|
+
let contract_id = env.register(Contract, ());
|
|
645
|
+
let client = ContractClient::new(&env, &contract_id);
|
|
646
|
+
let user = Address::generate(&env);
|
|
647
|
+
|
|
648
|
+
client.initialize(&user);
|
|
649
|
+
client.deposit(&user, &amount);
|
|
650
|
+
client.withdraw(&user, &amount);
|
|
651
|
+
|
|
652
|
+
prop_assert_eq!(client.balance(&user), 0);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
**Recommended workflow**: Use `cargo-fuzz` interactively to find deep bugs, then convert to `proptest` for regression prevention in CI.
|
|
659
|
+
|
|
660
|
+
## Differential Testing with Test Snapshots
|
|
661
|
+
|
|
662
|
+
Soroban automatically writes JSON snapshots at the end of every test to `test_snapshots/`, capturing events and final ledger state. Commit these to source control — diffs reveal unintended behavioral changes.
|
|
663
|
+
|
|
664
|
+
### Comparing Against Deployed Contracts
|
|
665
|
+
|
|
666
|
+
```rust
|
|
667
|
+
// Fetch deployed contract for comparison
|
|
668
|
+
// $ stellar contract fetch --id C... --out-file deployed.wasm
|
|
669
|
+
|
|
670
|
+
mod deployed {
|
|
671
|
+
soroban_sdk::contractimport!(file = "deployed.wasm");
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
#[test]
|
|
675
|
+
fn test_upgrade_compatibility() {
|
|
676
|
+
let env = Env::default();
|
|
677
|
+
env.mock_all_auths();
|
|
678
|
+
|
|
679
|
+
// Register both versions
|
|
680
|
+
let old_id = env.register_contract_wasm(None, deployed::WASM);
|
|
681
|
+
let new_id = env.register(NewContract, ());
|
|
682
|
+
|
|
683
|
+
let old_client = deployed::Client::new(&env, &old_id);
|
|
684
|
+
let new_client = NewContractClient::new(&env, &new_id);
|
|
685
|
+
|
|
686
|
+
let user = Address::generate(&env);
|
|
687
|
+
|
|
688
|
+
// Run identical operations and compare
|
|
689
|
+
old_client.initialize(&user);
|
|
690
|
+
new_client.initialize(&user);
|
|
691
|
+
|
|
692
|
+
assert_eq!(old_client.get_value(), new_client.get_value());
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
- **Docs**: [Differential Tests with Test Snapshots](https://developers.stellar.org/docs/build/guides/testing/differential-tests-with-test-snapshots)
|
|
697
|
+
|
|
698
|
+
## Fork Testing
|
|
699
|
+
|
|
700
|
+
Test against real production state using ledger snapshots:
|
|
701
|
+
|
|
702
|
+
```bash
|
|
703
|
+
# Create snapshot of deployed contract
|
|
704
|
+
stellar snapshot create --address C... --output json --out snapshot.json
|
|
705
|
+
|
|
706
|
+
# Optionally at a specific ledger
|
|
707
|
+
stellar snapshot create --address C... --ledger 12345678 --output json --out snapshot.json
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
```rust
|
|
711
|
+
#[test]
|
|
712
|
+
fn test_against_mainnet_state() {
|
|
713
|
+
let env = Env::from_ledger_snapshot_file("snapshot.json");
|
|
714
|
+
env.mock_all_auths();
|
|
715
|
+
|
|
716
|
+
let contract_id = /* contract address from snapshot */;
|
|
717
|
+
let client = ContractClient::new(&env, &contract_id);
|
|
718
|
+
|
|
719
|
+
// Test operations against real state
|
|
720
|
+
let result = client.get_value();
|
|
721
|
+
assert!(result > 0);
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
- **Docs**: [Fork Testing](https://developers.stellar.org/docs/build/guides/testing/fork-testing)
|
|
726
|
+
|
|
727
|
+
## Mutation Testing
|
|
728
|
+
|
|
729
|
+
Use `cargo-mutants` to verify test quality — modifies source code and checks that tests catch the changes.
|
|
730
|
+
|
|
731
|
+
```bash
|
|
732
|
+
cargo install --locked cargo-mutants
|
|
733
|
+
cargo mutants
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
**Output interpretation**:
|
|
737
|
+
- **CAUGHT**: Tests detected the mutation (good coverage)
|
|
738
|
+
- **MISSED**: Tests passed despite mutation (test gap — review `mutants.out/diff/`)
|
|
739
|
+
|
|
740
|
+
- **Docs**: [Mutation Testing](https://developers.stellar.org/docs/build/guides/testing/mutation-testing)
|
|
741
|
+
|
|
742
|
+
## Resource Profiling
|
|
743
|
+
|
|
744
|
+
Soroban uses a multidimensional resource model (CPU instructions, ledger reads/writes, bytes, events, rent).
|
|
745
|
+
|
|
746
|
+
### CLI Simulation
|
|
747
|
+
|
|
748
|
+
```bash
|
|
749
|
+
# Simulate contract invocation to see resource costs
|
|
750
|
+
stellar contract invoke \
|
|
751
|
+
--id CONTRACT_ID \
|
|
752
|
+
--source alice \
|
|
753
|
+
--network testnet \
|
|
754
|
+
--sim-only \
|
|
755
|
+
-- \
|
|
756
|
+
function_name --arg value
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Stellar Plus Profiler (Cheesecake Labs)
|
|
760
|
+
|
|
761
|
+
```typescript
|
|
762
|
+
import { StellarPlus } from 'stellar-plus';
|
|
763
|
+
|
|
764
|
+
const profilerPlugin = new StellarPlus.Utils.Plugins.sorobanTransaction.profiler();
|
|
765
|
+
// Collects CPU instructions, RAM, ledger reads/writes
|
|
766
|
+
// Aggregation: sum, average, standard deviation
|
|
767
|
+
// Output: CSV, formatted text tables
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
- **Docs**: https://docs.cheesecakelabs.com/stellar-plus/reference/utils/plugins/profiler-plugin
|
|
771
|
+
|
|
772
|
+
### Testing Checklist
|
|
773
|
+
|
|
774
|
+
- [ ] Unit tests cover all public functions
|
|
775
|
+
- [ ] Edge cases tested (zero amounts, max values, empty state)
|
|
776
|
+
- [ ] Authorization tested (correct signers required)
|
|
777
|
+
- [ ] Error conditions tested (invalid inputs, unauthorized)
|
|
778
|
+
- [ ] Events emission verified
|
|
779
|
+
- [ ] Storage TTL behavior validated
|
|
780
|
+
- [ ] Cross-contract interactions tested
|
|
781
|
+
- [ ] Fuzz tests for critical paths (deposits, withdrawals, swaps)
|
|
782
|
+
- [ ] Property-based tests for invariants
|
|
783
|
+
- [ ] Mutation testing confirms test quality
|
|
784
|
+
- [ ] Differential test snapshots committed to source control
|
|
785
|
+
- [ ] Integration tests against local network
|
|
786
|
+
- [ ] Testnet deployment verified before mainnet
|