From 308bc6a7b03ed28af04a4a05ce9443bdd311ce45 Mon Sep 17 00:00:00 2001 From: snkk2x-collab Date: Sat, 20 Jun 2026 06:51:49 +0800 Subject: [PATCH 1/2] feat: add mock score registry contract --- .gitignore | 3 + Contracts/README.md | 6 + Contracts/interfaces/README.md | 1 + Contracts/mock-score-registry/Cargo.toml | 25 +++ Contracts/mock-score-registry/README.md | 39 +++++ Contracts/mock-score-registry/src/lib.rs | 109 +++++++++++++ .../test/interface_version_returns_one.1.json | 76 +++++++++ .../returns_default_for_unknown_wallet.1.json | 76 +++++++++ .../test/stores_and_reads_mock_score.1.json | 151 ++++++++++++++++++ .../__tests__/mock-soroban.test.ts | 95 +++++++++++ Server/src/test-helpers/mock-soroban.ts | 7 + 11 files changed, 588 insertions(+) create mode 100644 Contracts/mock-score-registry/Cargo.toml create mode 100644 Contracts/mock-score-registry/README.md create mode 100644 Contracts/mock-score-registry/src/lib.rs create mode 100644 Contracts/mock-score-registry/test_snapshots/test/interface_version_returns_one.1.json create mode 100644 Contracts/mock-score-registry/test_snapshots/test/returns_default_for_unknown_wallet.1.json create mode 100644 Contracts/mock-score-registry/test_snapshots/test/stores_and_reads_mock_score.1.json create mode 100644 Server/src/test-helpers/__tests__/mock-soroban.test.ts create mode 100644 Server/src/test-helpers/mock-soroban.ts diff --git a/.gitignore b/.gitignore index 8e8739e..0ce1c66 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ api-docs.json # Vercel local link (may contain project ids) .vercel +# Rust build outputs +target/ + # OS .DS_Store Thumbs.db diff --git a/Contracts/README.md b/Contracts/README.md index 733e1df..a657ea3 100644 --- a/Contracts/README.md +++ b/Contracts/README.md @@ -6,6 +6,12 @@ Smart contracts for on-chain credit score attestation on Stellar. Stores portable credit scores attested by the ZCore oracle. Lenders and DeFi protocols can read scores directly from chain without calling the ZCore API. +## mock-score-registry + +TEST ONLY local mock for Server integration tests. It implements the same `get_score(wallet)` read shape and `interface_version()` value as `score-registry`, but has no admin authorization and exposes `set_mock_score(wallet, score, tier)` so tests can seed scores quickly. + +Build it from `Contracts/mock-score-registry` with `stellar contract build`, deploy to a local sandbox, then point Server tests at it with `withMockContractId("")`. + ### Functions | Function | Auth | Description | diff --git a/Contracts/interfaces/README.md b/Contracts/interfaces/README.md index 41e8aa4..71576e1 100644 --- a/Contracts/interfaces/README.md +++ b/Contracts/interfaces/README.md @@ -55,5 +55,6 @@ Topic: `("score_updated",)` — emitted on every `set_score` with previous and n ## Related - Implementation: `Contracts/score-registry/src/lib.rs` +- Local test mock: `Contracts/mock-score-registry/src/lib.rs` - Server bindings: `Server/src/services/soroban.service.ts` - Issue #32 tracking diff --git a/Contracts/mock-score-registry/Cargo.toml b/Contracts/mock-score-registry/Cargo.toml new file mode 100644 index 0000000..124d3a4 --- /dev/null +++ b/Contracts/mock-score-registry/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "mock-score-registry" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[dependencies] +soroban-sdk = "22.0.7" + +[dev-dependencies] +soroban-sdk = { version = "22.0.7", features = ["testutils"] } + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 +lto = true diff --git a/Contracts/mock-score-registry/README.md b/Contracts/mock-score-registry/README.md new file mode 100644 index 0000000..0c090dd --- /dev/null +++ b/Contracts/mock-score-registry/README.md @@ -0,0 +1,39 @@ +# Mock Score Registry + +TEST ONLY mock implementation of the `IZCoreScore` interface for local Server integration tests. + +Do not deploy this contract to mainnet. It has no admin authorization: any caller can set mock scores. + +## Functions + +| Function | Auth | Description | +|---|---|---| +| `init()` | none | Marks the mock as initialized | +| `set_mock_score(wallet, score, tier)` | none | Stores a mock score record for `wallet` | +| `get_score(wallet)` | public | Reads the configured mock score or a default rejected record | +| `interface_version()` | public | Returns `1` | + +## Local build + +```bash +cd Contracts/mock-score-registry +stellar contract build +``` + +## Local sandbox deploy + +```bash +stellar contract deploy \ + --wasm target/wasm32v1-none/release/mock_score_registry.wasm \ + --source alice \ + --network local + +stellar contract invoke \ + --id \ + --source alice \ + --network local \ + -- init +``` + +Set `SCORE_REGISTRY_CONTRACT_ID=` in `Server/.env` or call +`withMockContractId("")` from Server tests. diff --git a/Contracts/mock-score-registry/src/lib.rs b/Contracts/mock-score-registry/src/lib.rs new file mode 100644 index 0000000..180467a --- /dev/null +++ b/Contracts/mock-score-registry/src/lib.rs @@ -0,0 +1,109 @@ +#![no_std] + +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ScoreRecord { + pub score: u32, + pub tier: u32, + pub updated_at: u64, + pub valid_until: u64, +} + +const INIT_KEY: &str = "INIT"; +const INTERFACE_VERSION: u32 = 1; + +#[contract] +pub struct MockScoreRegistry; + +#[contractimpl] +impl MockScoreRegistry { + pub fn init(env: Env) { + env.storage().instance().set(&INIT_KEY, &true); + } + + pub fn interface_version(_env: Env) -> u32 { + INTERFACE_VERSION + } + + pub fn set_mock_score(env: Env, wallet: Address, score: u32, tier: u32) { + if score > 850 { + panic!("score exceeds maximum 850"); + } + if tier > 3 { + panic!("invalid tier"); + } + + let record = ScoreRecord { + score, + tier, + updated_at: env.ledger().timestamp(), + valid_until: 0, + }; + + env.storage().persistent().set(&wallet, &record); + } + + pub fn get_score(env: Env, wallet: Address) -> ScoreRecord { + env.storage() + .persistent() + .get(&wallet) + .unwrap_or(ScoreRecord { + score: 0, + tier: 0, + updated_at: 0, + valid_until: 0, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::{ + testutils::{Address as _, Ledger}, + Address, Env, + }; + + #[test] + fn stores_and_reads_mock_score() { + let env = Env::default(); + let contract_id = env.register(MockScoreRegistry, ()); + let client = MockScoreRegistryClient::new(&env, &contract_id); + let wallet = Address::generate(&env); + env.ledger().set_timestamp(1_718_000_000); + + client.init(); + client.set_mock_score(&wallet, &620, &3); + + let record = client.get_score(&wallet); + assert_eq!(record.score, 620); + assert_eq!(record.tier, 3); + assert!(record.updated_at > 0); + assert_eq!(record.valid_until, 0); + } + + #[test] + fn returns_default_for_unknown_wallet() { + let env = Env::default(); + let contract_id = env.register(MockScoreRegistry, ()); + let client = MockScoreRegistryClient::new(&env, &contract_id); + let wallet = Address::generate(&env); + + let record = client.get_score(&wallet); + assert_eq!(record.score, 0); + assert_eq!(record.tier, 0); + assert_eq!(record.updated_at, 0); + assert_eq!(record.valid_until, 0); + } + + #[test] + fn interface_version_returns_one() { + let env = Env::default(); + let contract_id = env.register(MockScoreRegistry, ()); + let client = MockScoreRegistryClient::new(&env, &contract_id); + + assert_eq!(client.interface_version(), 1); + } +} diff --git a/Contracts/mock-score-registry/test_snapshots/test/interface_version_returns_one.1.json b/Contracts/mock-score-registry/test_snapshots/test/interface_version_returns_one.1.json new file mode 100644 index 0000000..a90f00a --- /dev/null +++ b/Contracts/mock-score-registry/test_snapshots/test/interface_version_returns_one.1.json @@ -0,0 +1,76 @@ +{ + "generators": { + "address": 1, + "nonce": 0 + }, + "auth": [ + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/Contracts/mock-score-registry/test_snapshots/test/returns_default_for_unknown_wallet.1.json b/Contracts/mock-score-registry/test_snapshots/test/returns_default_for_unknown_wallet.1.json new file mode 100644 index 0000000..5655749 --- /dev/null +++ b/Contracts/mock-score-registry/test_snapshots/test/returns_default_for_unknown_wallet.1.json @@ -0,0 +1,76 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/Contracts/mock-score-registry/test_snapshots/test/stores_and_reads_mock_score.1.json b/Contracts/mock-score-registry/test_snapshots/test/stores_and_reads_mock_score.1.json new file mode 100644 index 0000000..60fc0a8 --- /dev/null +++ b/Contracts/mock-score-registry/test_snapshots/test/stores_and_reads_mock_score.1.json @@ -0,0 +1,151 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 1718000000, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "score" + }, + "val": { + "u32": 620 + } + }, + { + "key": { + "symbol": "tier" + }, + "val": { + "u32": 3 + } + }, + { + "key": { + "symbol": "updated_at" + }, + "val": { + "u64": 1718000000 + } + }, + { + "key": { + "symbol": "valid_until" + }, + "val": { + "u64": 0 + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "string": "INIT" + }, + "val": { + "bool": true + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/Server/src/test-helpers/__tests__/mock-soroban.test.ts b/Server/src/test-helpers/__tests__/mock-soroban.test.ts new file mode 100644 index 0000000..8a3dfec --- /dev/null +++ b/Server/src/test-helpers/__tests__/mock-soroban.test.ts @@ -0,0 +1,95 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { readOnChainScore } from "../../services/soroban.service"; +import { withMockContractId } from "../mock-soroban"; + +const ORIGINAL_CONTRACT_ID = process.env.SCORE_REGISTRY_CONTRACT_ID; + +vi.mock("@stellar/stellar-sdk", () => { + class MockAccount { + constructor( + public accountId: string, + public sequence: string + ) {} + } + + class MockContract { + constructor(public contractId: string) {} + + call() { + return { operation: "get_score" }; + } + } + + class MockServer { + async simulateTransaction() { + return { result: { retval: "mock-score" } }; + } + } + + class MockTransactionBuilder { + addOperation() { + return this; + } + + setTimeout() { + return this; + } + + build() { + return {}; + } + } + + return { + Account: MockAccount, + Address: { fromString: (wallet: string) => wallet }, + Contract: MockContract, + Networks: { + PUBLIC: "Public Global Stellar Network ; September 2015", + TESTNET: "Test SDF Network ; September 2015", + }, + TransactionBuilder: MockTransactionBuilder, + nativeToScVal: (value: unknown) => value, + rpc: { + Api: { isSimulationError: () => false }, + Server: MockServer, + }, + scValToNative: () => ({ + score: 620, + tier: 3, + updated_at: 1_718_000_000, + valid_until: 0, + }), + }; +}); + +describe("withMockContractId", () => { + afterEach(() => { + if (ORIGINAL_CONTRACT_ID === undefined) { + delete process.env.SCORE_REGISTRY_CONTRACT_ID; + } else { + process.env.SCORE_REGISTRY_CONTRACT_ID = ORIGINAL_CONTRACT_ID; + } + }); + + it("configures Server score reads to use a mock Soroban contract id", async () => { + withMockContractId("CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + + const score = await readOnChainScore( + "GD4ELZEONXZANIWRJAED5JPBN7KJG6ZQ5AV46HRLZRTEFNKWJP3UFREL" + ); + + expect(score).toEqual({ + score: 620, + tier: 3, + updatedAt: 1_718_000_000, + validUntil: 0, + }); + }); + + it("rejects blank mock contract ids", () => { + expect(() => withMockContractId(" ")).toThrow( + "Mock Soroban contract id is required" + ); + }); +}); diff --git a/Server/src/test-helpers/mock-soroban.ts b/Server/src/test-helpers/mock-soroban.ts new file mode 100644 index 0000000..e7c3b53 --- /dev/null +++ b/Server/src/test-helpers/mock-soroban.ts @@ -0,0 +1,7 @@ +export function withMockContractId(id: string): void { + if (!id.trim()) { + throw new Error("Mock Soroban contract id is required"); + } + + process.env.SCORE_REGISTRY_CONTRACT_ID = id; +} From 4d83bbdcd41ed43ed57e0b9d7e51d9b2b87861c9 Mon Sep 17 00:00:00 2001 From: snkk2x-collab Date: Sat, 20 Jun 2026 07:50:36 +0800 Subject: [PATCH 2/2] fix: align mock registry with upstream --- Contracts/interfaces/README.md | 3 +- Contracts/mock-score-registry/Cargo.toml | 2 +- Contracts/mock-score-registry/README.md | 49 ++++---- Contracts/mock-score-registry/src/lib.rs | 55 ++------- .../__tests__/mock-soroban.test.ts | 111 ++++-------------- Server/src/test-helpers/mock-soroban.ts | 20 +++- 6 files changed, 79 insertions(+), 161 deletions(-) diff --git a/Contracts/interfaces/README.md b/Contracts/interfaces/README.md index 71576e1..164eae1 100644 --- a/Contracts/interfaces/README.md +++ b/Contracts/interfaces/README.md @@ -55,6 +55,7 @@ Topic: `("score_updated",)` — emitted on every `set_score` with previous and n ## Related - Implementation: `Contracts/score-registry/src/lib.rs` -- Local test mock: `Contracts/mock-score-registry/src/lib.rs` +- **Test mock:** `Contracts/mock-score-registry/` (TEST ONLY — local/CI) - Server bindings: `Server/src/services/soroban.service.ts` +- Test helper: `Server/src/test-helpers/mock-soroban.ts` - Issue #32 tracking diff --git a/Contracts/mock-score-registry/Cargo.toml b/Contracts/mock-score-registry/Cargo.toml index 124d3a4..0be2173 100644 --- a/Contracts/mock-score-registry/Cargo.toml +++ b/Contracts/mock-score-registry/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib"] doctest = false [dependencies] diff --git a/Contracts/mock-score-registry/README.md b/Contracts/mock-score-registry/README.md index 0c090dd..abecff2 100644 --- a/Contracts/mock-score-registry/README.md +++ b/Contracts/mock-score-registry/README.md @@ -1,39 +1,44 @@ -# Mock Score Registry +# Mock Score Registry (TEST ONLY) -TEST ONLY mock implementation of the `IZCoreScore` interface for local Server integration tests. - -Do not deploy this contract to mainnet. It has no admin authorization: any caller can set mock scores. +Simplified Soroban contract for local integration tests. **Never deploy to mainnet.** ## Functions -| Function | Auth | Description | -|---|---|---| -| `init()` | none | Marks the mock as initialized | -| `set_mock_score(wallet, score, tier)` | none | Stores a mock score record for `wallet` | -| `get_score(wallet)` | public | Reads the configured mock score or a default rejected record | -| `interface_version()` | public | Returns `1` | +| Function | Description | +|----------|-------------| +| `init()` | Marks contract initialized (no admin auth) | +| `interface_version()` | Returns `1` | +| `set_mock_score(wallet, score, tier)` | Anyone can set a mock score | +| `get_score(wallet)` | Returns configured `ScoreRecord` | -## Local build +## Build ```bash cd Contracts/mock-score-registry +cargo test stellar contract build ``` -## Local sandbox deploy +## Local deploy (testnet/sandbox) ```bash stellar contract deploy \ --wasm target/wasm32v1-none/release/mock_score_registry.wasm \ - --source alice \ - --network local - -stellar contract invoke \ - --id \ - --source alice \ - --network local \ - -- init + --source-account YOUR_TESTNET_KEY \ + --network testnet +``` + +Use the returned contract ID in tests: + +```typescript +import { withMockContractId, clearMockContractId } from "../test-helpers/mock-soroban"; + +withMockContractId("C..."); +// run readOnChainScore(...) +clearMockContractId(); ``` -Set `SCORE_REGISTRY_CONTRACT_ID=` in `Server/.env` or call -`withMockContractId("")` from Server tests. +## Related + +- Production registry: `Contracts/score-registry/` +- Interface spec: `Contracts/interfaces/README.md` diff --git a/Contracts/mock-score-registry/src/lib.rs b/Contracts/mock-score-registry/src/lib.rs index 180467a..52de537 100644 --- a/Contracts/mock-score-registry/src/lib.rs +++ b/Contracts/mock-score-registry/src/lib.rs @@ -1,7 +1,8 @@ #![no_std] - use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; +/// TEST ONLY — do not deploy to mainnet. +/// Simplified score registry for local integration tests and CI. #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct ScoreRecord { @@ -12,7 +13,6 @@ pub struct ScoreRecord { } const INIT_KEY: &str = "INIT"; -const INTERFACE_VERSION: u32 = 1; #[contract] pub struct MockScoreRegistry; @@ -24,24 +24,17 @@ impl MockScoreRegistry { } pub fn interface_version(_env: Env) -> u32 { - INTERFACE_VERSION + 1 } pub fn set_mock_score(env: Env, wallet: Address, score: u32, tier: u32) { - if score > 850 { - panic!("score exceeds maximum 850"); - } - if tier > 3 { - panic!("invalid tier"); - } - + let updated_at = env.ledger().timestamp(); let record = ScoreRecord { score, tier, - updated_at: env.ledger().timestamp(), + updated_at, valid_until: 0, }; - env.storage().persistent().set(&wallet, &record); } @@ -61,49 +54,21 @@ impl MockScoreRegistry { #[cfg(test)] mod test { use super::*; - use soroban_sdk::{ - testutils::{Address as _, Ledger}, - Address, Env, - }; + use soroban_sdk::{testutils::Address as _, Env}; #[test] fn stores_and_reads_mock_score() { let env = Env::default(); + env.mock_all_auths(); let contract_id = env.register(MockScoreRegistry, ()); let client = MockScoreRegistryClient::new(&env, &contract_id); let wallet = Address::generate(&env); - env.ledger().set_timestamp(1_718_000_000); client.init(); - client.set_mock_score(&wallet, &620, &3); + client.set_mock_score(&wallet, &420, &2); let record = client.get_score(&wallet); - assert_eq!(record.score, 620); - assert_eq!(record.tier, 3); - assert!(record.updated_at > 0); - assert_eq!(record.valid_until, 0); - } - - #[test] - fn returns_default_for_unknown_wallet() { - let env = Env::default(); - let contract_id = env.register(MockScoreRegistry, ()); - let client = MockScoreRegistryClient::new(&env, &contract_id); - let wallet = Address::generate(&env); - - let record = client.get_score(&wallet); - assert_eq!(record.score, 0); - assert_eq!(record.tier, 0); - assert_eq!(record.updated_at, 0); - assert_eq!(record.valid_until, 0); - } - - #[test] - fn interface_version_returns_one() { - let env = Env::default(); - let contract_id = env.register(MockScoreRegistry, ()); - let client = MockScoreRegistryClient::new(&env, &contract_id); - - assert_eq!(client.interface_version(), 1); + assert_eq!(record.score, 420); + assert_eq!(record.tier, 2); } } diff --git a/Server/src/test-helpers/__tests__/mock-soroban.test.ts b/Server/src/test-helpers/__tests__/mock-soroban.test.ts index 8a3dfec..21823c7 100644 --- a/Server/src/test-helpers/__tests__/mock-soroban.test.ts +++ b/Server/src/test-helpers/__tests__/mock-soroban.test.ts @@ -1,95 +1,32 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { readOnChainScore } from "../../services/soroban.service"; -import { withMockContractId } from "../mock-soroban"; - -const ORIGINAL_CONTRACT_ID = process.env.SCORE_REGISTRY_CONTRACT_ID; - -vi.mock("@stellar/stellar-sdk", () => { - class MockAccount { - constructor( - public accountId: string, - public sequence: string - ) {} - } - - class MockContract { - constructor(public contractId: string) {} - - call() { - return { operation: "get_score" }; - } - } - - class MockServer { - async simulateTransaction() { - return { result: { retval: "mock-score" } }; - } - } - - class MockTransactionBuilder { - addOperation() { - return this; - } - - setTimeout() { - return this; - } - - build() { - return {}; - } - } - - return { - Account: MockAccount, - Address: { fromString: (wallet: string) => wallet }, - Contract: MockContract, - Networks: { - PUBLIC: "Public Global Stellar Network ; September 2015", - TESTNET: "Test SDF Network ; September 2015", - }, - TransactionBuilder: MockTransactionBuilder, - nativeToScVal: (value: unknown) => value, - rpc: { - Api: { isSimulationError: () => false }, - Server: MockServer, - }, - scValToNative: () => ({ - score: 620, - tier: 3, - updated_at: 1_718_000_000, - valid_until: 0, - }), - }; -}); - -describe("withMockContractId", () => { +import { afterEach, describe, expect, it } from "vitest"; +import { + clearMockContractId, + getEffectiveContractId, + isUsingMockContract, + withMockContractId, +} from "../../test-helpers/mock-soroban"; +import { getContractConfig } from "../../services/soroban.service"; + +describe("mock-soroban helper", () => { afterEach(() => { - if (ORIGINAL_CONTRACT_ID === undefined) { - delete process.env.SCORE_REGISTRY_CONTRACT_ID; - } else { - process.env.SCORE_REGISTRY_CONTRACT_ID = ORIGINAL_CONTRACT_ID; - } + clearMockContractId(); + delete process.env.SCORE_REGISTRY_CONTRACT_ID; }); - it("configures Server score reads to use a mock Soroban contract id", async () => { - withMockContractId("CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - - const score = await readOnChainScore( - "GD4ELZEONXZANIWRJAED5JPBN7KJG6ZQ5AV46HRLZRTEFNKWJP3UFREL" - ); + it("overrides contract id for tests", () => { + process.env.SCORE_REGISTRY_CONTRACT_ID = "C_PROD"; + withMockContractId("C_MOCK"); - expect(score).toEqual({ - score: 620, - tier: 3, - updatedAt: 1_718_000_000, - validUntil: 0, - }); + expect(getEffectiveContractId()).toBe("C_MOCK"); + expect(isUsingMockContract()).toBe(true); + expect(getContractConfig()?.contractId).toBe("C_MOCK"); }); - it("rejects blank mock contract ids", () => { - expect(() => withMockContractId(" ")).toThrow( - "Mock Soroban contract id is required" - ); + it("clears override", () => { + withMockContractId("C_MOCK"); + clearMockContractId(); + + expect(getEffectiveContractId()).toBeUndefined(); + expect(isUsingMockContract()).toBe(false); }); }); diff --git a/Server/src/test-helpers/mock-soroban.ts b/Server/src/test-helpers/mock-soroban.ts index e7c3b53..52553bf 100644 --- a/Server/src/test-helpers/mock-soroban.ts +++ b/Server/src/test-helpers/mock-soroban.ts @@ -1,7 +1,17 @@ -export function withMockContractId(id: string): void { - if (!id.trim()) { - throw new Error("Mock Soroban contract id is required"); - } +let mockContractIdOverride: string | null = null; - process.env.SCORE_REGISTRY_CONTRACT_ID = id; +export function withMockContractId(contractId: string): void { + mockContractIdOverride = contractId; +} + +export function clearMockContractId(): void { + mockContractIdOverride = null; +} + +export function getEffectiveContractId(): string | undefined { + return mockContractIdOverride ?? process.env.SCORE_REGISTRY_CONTRACT_ID; +} + +export function isUsingMockContract(): boolean { + return mockContractIdOverride !== null; }