Basic operations
In this section, we will explore basic operations on the Chromia blockchain, focusing on adding data through transactions. By the end of this tutorial, you will understand how to create users, make posts, and manage followers within your dapp.
Add data through transactions
In Chromia, you add data to the blockchain by sending transactions, which can contain one or more database operations known as "operations." Let’s define the operations for creating users, making posts, and following users.
Register a user
operation register_user(name) {
val account = register_account();
val user = create user ( name, account.id, account );
create follower ( user = user, follower = user );
}
The register_user
operation creates a new user. It takes the parameter name
(the user's name) and creates a user in the database. A public key, used as id
, gets passed directly to the constructor, thanks to Rell’s ability to identify types.
- Use a
byte_array
with a length of 32 or 64 for the public key.
An alternative notation would look like this:
create user( name = name, id = pubkey );
Make a post
operation make_post(user_id: byte_array, content: text) {
create post(
user @ { user_id },
content
);
}
The make_post
operation allows users to create posts. It requires user_id
(the ID of the posting user) and content
(the text of the post). Note that you don’t need to specify the timestamp, as it has a default value in the entity definition.
Follow a user
operation follow_user(user_id: byte_array, follow_id: byte_array) {
val user = user @ { user_id };
val follow = user @ { follow_id };
create follower(
user = follow,
follower = user
);
}
The follow_user
operation enables one user to follow another. It requires user_id
(the ID of the user who wants to follow) and follow_id
(the ID of the user to be followed). The operation fetches both users from the database and creates a follower
entity to establish the relationship.
Unfollow a user
To unfollow a user, you need to delete the follower
entity.
operation unfollow_user(user_id: byte_array, unfollow_id: byte_array) {
val user = user @ { user_id };
val unfollow = user @ { unfollow_id };
delete follower @? { .user == unfollow, .follower == user };
}
Test the operations
Let’s ensure your dapp code works as intended by diving into unit tests. Testing is essential to verify that your operations produce the expected results.
Set up the test module
- Navigate to the test folder in your project's directory. If the folder doesn’t exist, create it.
- Create a file named
my_news_feed_test.rell
inside the test folder. This file will contain your test cases. - Add the following header to
my_news_feed_test.rell
:
@test module;
import main.*;
- The
@test
module declaration indicates that this module contains test code. - The
import main.*;
line imports everything from themain
module, making your dapp code accessible for testing.
Test dapp
Let’s examine some test cases to ensure that your dapp functions correctly. We will test the creation of users, following relationships, and posts.
@test module;
import main.*;
val alice = rell.test.pubkeys.alice;
val bob = rell.test.pubkeys.bob;
function test_create_entities() {
// Create two users: Alice and Bob
rell.test.tx()
.op(create_user("Alice", alice))
.op(create_user("Bob", bob))
.run();
// Check that there are two users in the user table
assert_equals(user @ { } (@sum 1), 2);
// Alice follows Bob and creates a post
rell.test.tx()
.op(follow_user(alice, bob))
.op(make_post(alice, "My post"))
.run();
// Verify that there is one follower and one post
assert_equals(follower @ { } (@sum 1), 1);
assert_equals(post @ { } (@sum 1), 1);
// Alice unfollows Bob and verify there are no followers
rell.test.tx()
.op(unfollow_user(alice, bob))
.run();
assert_equals(follower @ { } (@sum 1), 0);
}
In this test case:
- We use two public keys,
alice
andbob
, from the Rell test framework as user IDs. - The function
test_create_entities
, prefixed withtest_
to indicate that it is a test case, runs a series of transactions to create users and manage their interactions. - After each transaction, we use the
assert_equals
function to check if the expected number of entities is present in the corresponding tables. We employ the@sum
function to aggregate values from the table, setting it to1
because we are only interested in counting the number of entities.
Update tests with FT authentication
To get your tests up and running with FT4 authentication, you need to understand how it works. For each authenticated operation, call either auth.ft_auth
or auth.evm_auth
in the same transaction directly before the operation. Use the ft_auth_operation_for
function from the ft
library's test module:
import lib.ft4.test.core.{ ft_auth_operation_for };
Inject these function calls into all transactions that involve make_post
or follow_user
, and remove the user_id
argument from these operations. For example, in test_create_entities
:
function test_create_entities() {
rell.test.tx()
.op(create_user("Alice", alice))
.op(create_user("Bob", bob))
.run();
assert_equals(user @ { } (@sum 1), 2);
rell.test.tx()
.op(ft_auth_operation_for(alice)) // Auth operation
.op(follow_user(bob)) // Argument removed
.op(ft_auth_operation_for(alice)) // Auth operation
.op(make_post("My post")) // Argument removed
.sign(alice_kp)
.run();
assert_true(is_following(alice, bob));
assert_equals(follower @ { } (@sum 1), 1);
assert_equals(post @ { } (@sum 1), 1);
}
Updating test_input_verification
showcases the power of this approach, making it difficult to impersonate others. In the case where Charlie tries to make a post, he cannot proceed because he cannot create the auth operation. The modified test now looks like this:
function test_input_verification() {
rell.test.tx()
.op(create_user("Alice", alice))
.op(create_user("Bob", bob)).run();
// Alice cannot be impersonated by Bob
rell.test.tx()
.op(ft_auth_operation_for(alice)) // <-- malicious auth operation
.op(follow_user(bob)) // <-- argument removed
.sign(bob_kp)
.run_must_fail();
rell.test.tx()
.op(ft_auth_operation_for(alice)) // <-- malicious auth operation
.op(make_post("My malicious post")) // <-- argument removed
.sign(bob_kp)
.run_must_fail();
// Charlie cannot be followed by Alice since he doesn't exist
val f1 = rell.test.tx()
.op(ft_auth_operation_for(alice)) // <-- auth operation
.op(follow_user(charlie)) // <-- argument removed
.sign(alice_kp)
.run_must_fail();
assert_true(f1.message.contains("does not exist"));
// A post cannot be created by Charlie since he does not exist
val f2 = rell.test.tx()
//.op(ft_auth_operation_for(charlie)) // <-- Cannot create auth operation
.op(make_post("My secret post")) // <-- argument removed
.sign(charlie_kp)
.run_must_fail();
assert_true(f2.message.contains("Expected at least two operations"));
}
Make sure to also update test_follower_calculation
and test_pagination_of_posts
:
function test_follower_calculation() {
rell.test.tx()
.op(create_user("Alice", alice))
.op(create_user("Bob", bob))
.op(create_user("Charlie", charlie))
.run();
rell.test.tx()
.op(ft_auth_operation_for(alice))
.op(follow_user(bob))
.op(ft_auth_operation_for(alice))
.op(follow_user(charlie))
.sign(alice_kp)
.run();
assert_true(is_following(alice, bob));
assert_true(is_following(alice, charlie));
assert_equals(get_following_count(alice), 2);
assert_equals(get_following_count(bob), 0);
assert_equals(get_followers_count(alice), 0);
assert_equals(get_followers_count(bob), 1);
}
function test_pagination_of_posts() {
rell.test.tx()
.op(create_user("Alice", alice))
.op(create_user("Bob", bob)).run();
rell.test.tx()
.op(ft_auth_operation_for(alice))
.op(follow_user(bob))
.sign(alice_kp)
.run();
for (i in range(5)) {
rell.test.tx()
.op(ft_auth_operation_for(bob))
.op(make_post("Content %d".format(i)))
.sign(bob_kp)
.run();
}
val initial_posts = get_posts(alice, 0, 4);
assert_equals(initial_posts.pointer, 4);
assert_equals(initial_posts.posts.size(), 4);
val last_posts = get_posts(alice, initial_posts.pointer, 4);
assert_equals(last_posts.pointer, 5);
assert_equals(last_posts.posts.size(), 1);
}
Run the tests
Now you can run the tests to verify your dapp's functionality.
-
Open a terminal and navigate to your project's root directory.
-
Execute the following command to run the tests:
chr test
This command runs the tests located in the test: modules
defined in chromia.yml
. If all tests pass, you will see a confirmation that your dapp's functionality works as expected.
Congratulations! You have successfully added unit tests to your dapp, ensuring its reliability.