Skip to content

Commit

Permalink
[move] TestScenario code for mocking multi-transaction scenarios wi…
Browse files Browse the repository at this point in the history
…th realistic access control

The idea here is to mimic the access control model of Sui (e.g. a tx can only touch objects owned by its sender) in pure Move. This will allow application developers to discover bugs about their sharing logic in Move unit tests, as well as to test the basics like whether an object was transferred to the correct user. See inline comments in `TestScenario` for more detail, but the basics are

- Create a test scenario with `TestScenario::begin(sender_address)`.
- Advance to the next tx in the scenario with `TestScenario::next_tx(scenario, sender_address)`
- The sender of the current tx can remove objects transferred to them by previous txes with `TestScenario::remove_object`
- If the sender wants to use a removed object in future txes, it must return it with `TestScenario::return_object`
  • Loading branch information
sblackshear committed Feb 21, 2022
1 parent fbac694 commit 3f9968c
Show file tree
Hide file tree
Showing 16 changed files with 936 additions and 145 deletions.
64 changes: 63 additions & 1 deletion sui_programmability/examples/sources/Hero.move
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,66 @@ module Examples::Hero {
assert!(hero_strength(hero) == strength, ASSERT_ERR);
}

}
#[test_only]
public fun delete_hero_for_testing(hero: Hero) {
let Hero { id, hp: _, experience: _, sword } = hero;
ID::delete(id);
let sword = Option::destroy_some(sword);
let Sword { id, magic: _, strength: _ } = sword;
ID::delete(id)
}

#[test_only]
public fun delete_game_admin_for_testing(admin: GameAdmin) {
let GameAdmin { id, boars_created: _, potions_created: _ } = admin;
ID::delete(id);
}

#[test]
public fun slay_boar_test() {
use Examples::TrustedCoin::{Self, EXAMPLE};
use FastX::Coin::{Self, TreasuryCap};
use FastX::TestScenario;

let admin = Address::new(ADMIN);
let player = Address::dummy_with_hint(0);

let scenario = &mut TestScenario::begin(&admin);
// Run the module initializers
{
let ctx = TestScenario::ctx(scenario);
TrustedCoin::test_init(ctx);
init(ctx);
};
// Admin mints 500 coins and sends them to the Player so they can buy game items
TestScenario::next_tx(scenario, &admin);
{
let treasury_cap = TestScenario::remove_object<TreasuryCap<EXAMPLE>>(scenario);
let ctx = TestScenario::ctx(scenario);
let coins = Coin::mint(500, &mut treasury_cap, ctx);
Coin::transfer(coins, copy player);
TestScenario::return_object(scenario, treasury_cap);
};
// Player purchases a hero with the coins
TestScenario::next_tx(scenario, &player);
{
let coin = TestScenario::remove_object<Coin<EXAMPLE>>(scenario);
acquire_hero(coin, TestScenario::ctx(scenario));
};
// Admin sends a boar to the Player
TestScenario::next_tx(scenario, &admin);
{
let admin_cap = TestScenario::remove_object<GameAdmin>(scenario);
send_boar(&mut admin_cap, 10, 10, *Address::bytes(&player), TestScenario::ctx(scenario));
TestScenario::return_object(scenario, admin_cap)
};
// Player slays the boar!
TestScenario::next_tx(scenario, &player);
{
let hero = TestScenario::remove_object<Hero>(scenario);
let boar = TestScenario::remove_object<Boar>(scenario);
slay(&mut hero, boar, TestScenario::ctx(scenario));
TestScenario::return_object(scenario, hero)
};
}
}
16 changes: 16 additions & 0 deletions sui_programmability/examples/sources/TicTacToe.move
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ module Examples::TicTacToe {
ID::delete(id);
}

public fun get_status(game: &TicTacToe): u8 {
game.game_status
}

fun mint_mark(cap: &mut MarkMintCap, row: u64, col: u64, ctx: &mut TxContext): Mark {
if (cap.remaining_supply == 0) {
abort NO_MORE_MARK
Expand Down Expand Up @@ -248,4 +252,16 @@ module Examples::TicTacToe {
let Mark { id, player: _, row: _, col: _ } = mark;
ID::delete(id);
}

public fun mark_player(mark: &Mark): &Address {
&mark.player
}

public fun mark_row(mark: &Mark): u64 {
mark.row
}

public fun mark_col(mark: &Mark): u64 {
mark.col
}
}
6 changes: 6 additions & 0 deletions sui_programmability/examples/sources/TrustedCoin.move
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,10 @@ module Examples::TrustedCoin {
fun transfer(treasury_cap: TreasuryCap<EXAMPLE>, recipient: vector<u8>, _ctx: &mut TxContext) {
Coin::transfer_cap<EXAMPLE>(treasury_cap, Address::new(recipient));
}

#[test_only]
/// Wrapper of module initializer for testing
public fun test_init(ctx: &mut TxContext) {
init(ctx)
}
}
86 changes: 47 additions & 39 deletions sui_programmability/examples/tests/TicTacToeTests.move
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
#[test_only]
module Examples::TicTacToeTests {
use FastX::TxContext::{Self, TxContext};
use FastX::TestHelper;
use FastX::Address::{Self, Address};
use FastX::TestScenario::{Self, Scenario};
use Examples::TicTacToe::{Self, Mark, MarkMintCap, TicTacToe, Trophy};

const SEND_MARK_FAILED: u64 = 0;
const UNEXPECTED_WINNER: u64 = 1;
const MARK_PLACEMENT_FAILED: u64 = 2;
const IN_PROGRESS: u8 = 0;
const X_WIN: u8 = 1;

#[test]
fun play_tictactoe() {
let admin_ctx = TxContext::dummy_with_hint(0);
let player_x_ctx = TxContext::dummy_with_hint(2);
let player_o_ctx = TxContext::dummy_with_hint(1);

// Create a game under admin.
TicTacToe::create_game(
TxContext::get_signer_address(&player_x_ctx),
TxContext::get_signer_address(&player_o_ctx),
&mut admin_ctx,
);
let game = TestHelper::get_last_received_object<TicTacToe>(&admin_ctx);

let cap_x = TestHelper::get_last_received_object<MarkMintCap>(&player_x_ctx);
let cap_o = TestHelper::get_last_received_object<MarkMintCap>(&player_o_ctx);
let admin = Address::dummy_with_hint(0);
let player_x = Address::dummy_with_hint(1);
let player_o = Address::dummy_with_hint(2);

let scenario = &mut TestScenario::begin(&admin);
// Admin creates a game
TicTacToe::create_game(copy player_x, copy player_o, TestScenario::ctx(scenario));
// Player1 places an X in (1, 1).
place_mark(&mut game, &mut cap_x, 1, 1, &mut admin_ctx, &mut player_x_ctx);
place_mark(1, 1, &admin, &player_x, scenario);
/*
Current game board:
_|_|_
Expand All @@ -35,7 +29,7 @@ module Examples::TicTacToeTests {
*/

// Player2 places an O in (0, 0).
place_mark(&mut game, &mut cap_x, 0, 0, &mut admin_ctx, &mut player_o_ctx);
place_mark(0, 0, &admin, &player_o, scenario);
/*
Current game board:
O|_|_
Expand All @@ -44,7 +38,7 @@ module Examples::TicTacToeTests {
*/

// Player1 places an X in (0, 2).
place_mark(&mut game, &mut cap_x, 0, 2, &mut admin_ctx, &mut player_x_ctx);
place_mark(0, 2, &admin, &player_x, scenario);
/*
Current game board:
O|_|X
Expand All @@ -53,7 +47,7 @@ module Examples::TicTacToeTests {
*/

// Player2 places an O in (1, 0).
place_mark(&mut game, &mut cap_x, 1, 0, &mut admin_ctx, &mut player_o_ctx);
let status = place_mark(1, 0, &admin, &player_o, scenario);
/*
Current game board:
O|_|X
Expand All @@ -62,36 +56,50 @@ module Examples::TicTacToeTests {
*/

// Opportunity for Player1! Player1 places an X in (2, 0).
place_mark(&mut game, &mut cap_x, 2, 0, &mut admin_ctx, &mut player_x_ctx);
assert!(status == IN_PROGRESS, 1);
status = place_mark(2, 0, &admin, &player_x, scenario);
assert!(status == X_WIN, 2);
/*
Current game board:
O|_|X
O|X|_
X| |
*/
// Check that X has won!
let trophy = TestHelper::get_last_received_object<Trophy>(&mut player_x_ctx);
TicTacToe::delete_trophy(trophy, &mut player_x_ctx);

// Cleanup and delete all objects in the game.
TicTacToe::delete_game(game, &mut admin_ctx);
TicTacToe::delete_cap(cap_x, &mut player_x_ctx);
TicTacToe::delete_cap(cap_o, &mut player_o_ctx);
// Check that X has won!
TestScenario::next_tx(scenario, &player_x);
let trophy = TestScenario::remove_object<Trophy>(scenario);
TestScenario::return_object(scenario, trophy)
}

fun place_mark(
game: &mut TicTacToe,
cap: &mut MarkMintCap,
row: u64,
col: u64,
admin_ctx: &mut TxContext,
player_ctx: &mut TxContext,
) {
// Step 1: Create a mark and send it to the game.
TicTacToe::send_mark_to_game(cap, TxContext::get_signer_address(admin_ctx), row, col, player_ctx);

// Step 2: Game places the mark on the game board.
let mark = TestHelper::get_last_received_object<Mark>(admin_ctx);
TicTacToe::place_mark(game, mark, admin_ctx);
admin: &Address,
player: &Address,
scenario: &mut Scenario,
): u8 {
// Step 1: player creates a mark and sends it to the game.
TestScenario::next_tx(scenario, player);
{
let cap = TestScenario::remove_object<MarkMintCap>(scenario);
TicTacToe::send_mark_to_game(&mut cap, *admin, row, col, TestScenario::ctx(scenario));
TestScenario::return_object(scenario, cap);
};
// Step 2: Admin places the received mark on the game board.
TestScenario::next_tx(scenario, admin);
let status;
{
let game = TestScenario::remove_object<TicTacToe>(scenario);
let mark = TestScenario::remove_object<Mark>(scenario);
assert!(TicTacToe::mark_player(&mark) == player, 0);
assert!(TicTacToe::mark_row(&mark) == row, 1);
assert!(TicTacToe::mark_col(&mark) == col, 2);
TicTacToe::place_mark(&mut game, mark, TestScenario::ctx(scenario));
status = TicTacToe::get_status(&game);
TestScenario::return_object(scenario, game);
};
// return the game status
status
}
}
25 changes: 22 additions & 3 deletions sui_programmability/framework/sources/Address.move
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,42 @@ module FastX::Address {
Signer { inner: new(bytes) }
}

#[test_only]
/// Create a `Signer` from `a` for testing
public fun new_signer_from_address(a: Address): Signer {
Signer { inner: a }
}

#[test_only]
/// Create a dummy `Signer` for testing
public fun dummy_signer(): Signer {
new_signer(x"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
}

#[test_only]
/// Create a dummy `Signer` for testing
/// All bytes will be 0 except the last byte, which will be `hint`.
public fun dummy_signer_with_hint(hint: u8): Signer {
fun bytes_with_hint(hint: u8): vector<u8> {
let bytes = Vector::empty<u8>();
let i = 0;
while (i < ADDRESS_LENGTH - 1) {
Vector::push_back(&mut bytes, 0u8);
i = i + 1;
};
Vector::push_back(&mut bytes, hint);
new_signer(bytes)
bytes
}

#[test_only]
/// Create a dummy `Signer` for testing
/// All bytes will be 0 except the last byte, which will be `hint`.
public fun dummy_signer_with_hint(hint: u8): Signer {
new_signer(bytes_with_hint(hint))
}

#[test_only]
/// Create a dummy `Address` for testing
/// All bytes will be 0 except the last byte, which will be `hint`.
public fun dummy_with_hint(hint: u8): Address {
new(bytes_with_hint(hint))
}
}
9 changes: 8 additions & 1 deletion sui_programmability/framework/sources/ID.move
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// FastX object identifiers
module FastX::ID {
use Std::BCS;
// TODO(): bring this back
//friend FastX::TxContext;

Expand Down Expand Up @@ -59,11 +60,17 @@ module FastX::ID {
get_version(obj) == INITIAL_VERSION
}

/// Get the raw bytes of `i`
/// Get the raw bytes of `i` in its underlying representation
// TODO: we should probably not expose that this is an `address`
public fun get_bytes(i: &IDBytes): &address {
&i.bytes
}

/// Get the raw bytes of `i` as a vector
public fun get_bytes_as_vec(i: &IDBytes): vector<u8> {
BCS::to_bytes(get_bytes(i))
}

/// Get the ID for `obj`. Safe because fastX has an extra
/// bytecode verifier pass that forces every struct with
/// the `key` ability to have a distinguished `ID` field.
Expand Down
17 changes: 0 additions & 17 deletions sui_programmability/framework/sources/TestHelper.move

This file was deleted.

Loading

0 comments on commit 3f9968c

Please sign in to comment.