From 77273388f2e2a21de37b2896c44862d3f82e2bbc Mon Sep 17 00:00:00 2001 From: olowo Date: Mon, 28 Apr 2025 14:30:07 +0100 Subject: [PATCH 1/3] feat: implement new content funtion --- src/base/types.cairo | 2 +- src/chainlib/ChainLib.cairo | 77 ++++++++++++++++++++++++++++++++-- src/interfaces/IChainLib.cairo | 10 +++++ tests/test_ChainLib.cairo | 70 ++++++++++++++++++++++++++++++- 4 files changed, 153 insertions(+), 6 deletions(-) diff --git a/src/base/types.cairo b/src/base/types.cairo index 163d40f..f0c4797 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -9,7 +9,7 @@ pub struct TokenBoundAccount { pub created_at: u64, pub updated_at: u64, } -#[derive(Drop, Serde, starknet::Store)] +#[derive(Drop, Serde, starknet::Store, Clone)] pub struct User { pub id: u256, pub username: felt252, diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index a84186f..a766117 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -4,7 +4,9 @@ pub mod ChainLib { Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, StoragePointerWriteAccess, }; - use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; + use starknet::{ + ContractAddress, get_block_timestamp, get_caller_address, contract_address_const + }; use crate::interfaces::IChainLib::IChainLib; use crate::base::types::{TokenBoundAccount, User, Role, Rank}; @@ -48,7 +50,9 @@ pub mod ChainLib { users: Map, creators_content: Map::, content: Map::, - content_tags: Map::> + content_tags: Map::>, + next_content_id: felt252, + user_by_address: Map, } @@ -63,6 +67,7 @@ pub mod ChainLib { pub enum Event { TokenBoundAccountCreated: TokenBoundAccountCreated, UserCreated: UserCreated, + ContentRegistered: ContentRegistered, } #[derive(Drop, starknet::Event)] @@ -75,6 +80,14 @@ pub mod ChainLib { pub id: u256, } + + #[derive(Drop, starknet::Event)] + pub struct ContentRegistered { + pub content_id: felt252, + pub creator: ContractAddress + } + + #[abi(embed_v0)] impl ChainLibNetImpl of IChainLib { /// @notice Creates a new token-bound account. @@ -164,10 +177,15 @@ pub mod ChainLib { }; // Store the new user in the users mapping. + self.users.write(user_id, new_user); + // Also store the user in the user_by_address mapping for address-based lookups. + let user_for_address = self.users.read(user_id); + self.user_by_address.write(user_for_address.wallet_address, user_for_address); + // Increment the user ID counter for the next registration. - self.current_account_id.write(user_id + 1); + self.user_id.write(user_id + 1); // Emit an event to notify about the new user registration. self.emit(UserCreated { id: user_id }); @@ -214,5 +232,58 @@ pub mod ChainLib { let admin = self.admin.read(); admin } + + + /// @notice Registers new content in the system. + /// @dev Only users with WRITER role can register content. + /// @param self The contract state reference. + /// @param title The title of the content (cannot be empty). + /// @param description The description of the content. + /// @param content_type The type of content being registered. + /// @param category The category the content belongs to. + /// @return felt252 Returns the unique identifier of the registered content. + fn register_content( + ref self: ContractState, + title: felt252, + description: felt252, + content_type: ContentType, + category: Category + ) -> felt252 { + assert!(title != 0, "Title cannot be empty"); + assert!(description != 0, "Description cannot be empty"); + + let creator = get_caller_address(); + let user = self.user_by_address.read(creator); + + assert!(user.role == Role::WRITER, "Only WRITER can post content"); + + let content_id = self.next_content_id.read(); + + let content_metadata = ContentMetadata { + content_id: content_id, + title: title, + description: description, + content_type: content_type, + creator: creator, + category: category + }; + + self.content.write(content_id, content_metadata); + self.creators_content.write(creator, content_metadata); + self.next_content_id.write(content_id + 1); + + self.emit(ContentRegistered { content_id: content_id, creator: creator }); + + content_id + } + + + fn get_content(ref self: ContractState, content_id: felt252) -> ContentMetadata { + let content_metadata = self.content.read(content_id); + + assert!(content_metadata.content_id == content_id, "Content does not exist"); + + content_metadata + } } } diff --git a/src/interfaces/IChainLib.cairo b/src/interfaces/IChainLib.cairo index 4f90038..1ddc305 100644 --- a/src/interfaces/IChainLib.cairo +++ b/src/interfaces/IChainLib.cairo @@ -1,5 +1,6 @@ use starknet::ContractAddress; use crate::base::types::{TokenBoundAccount, User, Role, Rank}; +use crate::chainlib::ChainLib::ChainLib::{Category, ContentType, ContentMetadata}; #[starknet::interface] pub trait IChainLib { @@ -20,5 +21,14 @@ pub trait IChainLib { fn retrieve_user_profile(ref self: TContractState, user_id: u256) -> User; fn getAdmin(self: @TContractState) -> ContractAddress; fn is_verified(ref self: TContractState, user_id: u256) -> bool; + fn register_content( + ref self: TContractState, + title: felt252, + description: felt252, + content_type: ContentType, + category: Category + ) -> felt252; + + fn get_content(ref self: TContractState, content_id: felt252) -> ContentMetadata; } diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index 5fbbb1f..0ba4b83 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -2,12 +2,16 @@ use chain_lib::chainlib::ChainLib; use chain_lib::interfaces::IChainLib::{IChainLib, IChainLibDispatcher, IChainLibDispatcherTrait}; -use snforge_std::{CheatSpan, ContractClassTrait, DeclareResultTrait, cheat_caller_address, declare}; +use snforge_std::{ + CheatSpan, ContractClassTrait, DeclareResultTrait, cheat_caller_address, declare, spy_events, + EventSpy, EventSpyAssertionsTrait +}; use starknet::ContractAddress; use starknet::class_hash::ClassHash; use starknet::contract_address::contract_address_const; use starknet::testing::{set_caller_address, set_contract_address}; use chain_lib::base::types::{Role, Rank}; +use chain_lib::chainlib::ChainLib::ChainLib::{ContentType, Category, ContentMetadata}; fn setup() -> (ContractAddress, ContractAddress) { @@ -115,7 +119,7 @@ fn test_verify_user() { } #[test] -#[should_panic(expected: 'Only admin can verify users')] +#[should_panic(expected: ('Only admin can verify users',))] fn test_verify_user_not_admin() { let (contract_address, _) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; @@ -138,3 +142,65 @@ fn test_verify_user_not_admin() { let verified_user = dispatcher.retrieve_user_profile(account_id); assert(verified_user.verified, 'Not Verified'); } + + +#[test] +fn test_register_content() { + let (contract_address, _) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + + let mut spy = spy_events(); + + let title: felt252 = 'My Content'; + let description: felt252 = 'This is a test content'; + let content_type: ContentType = ContentType::Text; + let category: Category = Category::Education; + let caller_address: ContractAddress = contract_address_const::<'creator'>(); + + // Register a user with WRITER role + let username: felt252 = 'John'; + let role: Role = Role::WRITER; + let rank: Rank = Rank::BEGINNER; + let metadata: felt252 = 'john is a boy'; + + // Set caller address for user registration + cheat_caller_address(contract_address, caller_address, CheatSpan::Indefinite); + + // Call register_user + let user_id = dispatcher.register_user(username, role.clone(), rank.clone(), metadata); + + // Verify user registration + let user = dispatcher.retrieve_user_profile(user_id); + assert(user.role == Role::WRITER, 'User role not WRITER'); + + // Register content + let content_id = dispatcher.register_content(title, description, content_type, category); + + // Verify content ID starts from 0 + assert(content_id == 0, 'content_id should start from 0'); + + // Retrieve and verify content metadata + let content = dispatcher.get_content(content_id); + assert(content.content_id == content_id, 'content_id mismatch'); + assert(content.title == title, 'title mismatch'); + assert(content.description == description, 'description mismatch'); + assert(content.content_type == content_type, 'content_type mismatch'); + assert(content.creator == caller_address, 'creator mismatch'); + assert(content.category == category, 'category mismatch'); + + // Verify that the ContentRegistered event was emitted with correct parameters + spy + .assert_emitted( + @array![ + ( + contract_address, + chain_lib::chainlib::ChainLib::ChainLib::Event::ContentRegistered( + chain_lib::chainlib::ChainLib::ChainLib::ContentRegistered { + content_id: content_id, creator: caller_address + } + ) + ) + ] + ); +} + From 1490963e5b251c94546696ccfb8638e1b1958397 Mon Sep 17 00:00:00 2001 From: olowo Date: Mon, 28 Apr 2025 16:37:37 +0100 Subject: [PATCH 2/3] scarb fmt --- src/chainlib/ChainLib.cairo | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index a766117..bd3ee42 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -234,14 +234,14 @@ pub mod ChainLib { } - /// @notice Registers new content in the system. - /// @dev Only users with WRITER role can register content. - /// @param self The contract state reference. - /// @param title The title of the content (cannot be empty). - /// @param description The description of the content. - /// @param content_type The type of content being registered. - /// @param category The category the content belongs to. - /// @return felt252 Returns the unique identifier of the registered content. + /// @notice Registers new content in the system. + /// @dev Only users with WRITER role can register content. + /// @param self The contract state reference. + /// @param title The title of the content (cannot be empty). + /// @param description The description of the content. + /// @param content_type The type of content being registered. + /// @param category The category the content belongs to. + /// @return felt252 Returns the unique identifier of the registered content. fn register_content( ref self: ContractState, title: felt252, From a99a0015b9d3a3f7c0c53bc1ee8bb6109330ff4d Mon Sep 17 00:00:00 2001 From: olowo Date: Wed, 30 Apr 2025 08:10:09 +0100 Subject: [PATCH 3/3] add test --- src/chainlib/ChainLib.cairo | 1 - tests/test_ChainLib.cairo | 326 ++++++++++++++++++++++++++++++++++++ 2 files changed, 326 insertions(+), 1 deletion(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index bd3ee42..46d68b0 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -282,7 +282,6 @@ pub mod ChainLib { let content_metadata = self.content.read(content_id); assert!(content_metadata.content_id == content_id, "Content does not exist"); - content_metadata } } diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index 0ba4b83..2812c86 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -204,3 +204,329 @@ fn test_register_content() { ); } +#[test] +fn test_register_content_with_different_types() { + let (contract_address, _) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + + let mut spy = spy_events(); + + // Test with different content types and categories + let title: felt252 = 'Video Tutorial'; + let description: felt252 = 'Cairo programming'; + let content_type: ContentType = ContentType::Video; + let category: Category = Category::Software; + let creator_address: ContractAddress = contract_address_const::<'video_creator'>(); + + // Register a user with WRITER role + let username: felt252 = 'VideoCreator'; + let role: Role = Role::WRITER; + let rank: Rank = Rank::INTERMEDIATE; + let metadata: felt252 = 'Professional video creator'; + + // Set caller address for user registration + cheat_caller_address(contract_address, creator_address, CheatSpan::Indefinite); + + // Call register_user + let user_id = dispatcher.register_user(username, role.clone(), rank.clone(), metadata); + + // Register content + let content_id = dispatcher.register_content(title, description, content_type, category); + + // Verify content was registered correctly + let content = dispatcher.get_content(content_id); + assert(content.content_type == ContentType::Video, 'content_type mismatch'); + assert(content.category == Category::Software, 'category mismatch'); + assert(content.creator == creator_address, 'creator mismatch'); + + // Verify event emission + spy + .assert_emitted( + @array![ + ( + contract_address, + chain_lib::chainlib::ChainLib::ChainLib::Event::ContentRegistered( + chain_lib::chainlib::ChainLib::ChainLib::ContentRegistered { + content_id: content_id, creator: creator_address + } + ) + ) + ] + ); + + // Register another content with different type + let image_title: felt252 = 'Infographic'; + let image_description: felt252 = 'Visual of Cairo concepts'; + let image_content_type: ContentType = ContentType::Image; + let image_category: Category = Category::Education; + + let image_content_id = dispatcher + .register_content(image_title, image_description, image_content_type, image_category); + + // Verify second content was registered with a new ID + assert(image_content_id == content_id + 1, 'content_id not incremented'); + + let image_content = dispatcher.get_content(image_content_id); + assert(image_content.content_type == ContentType::Image, 'image type mismatch'); + assert(image_content.category == Category::Education, 'image category mismatch'); +} + +#[test] +#[should_panic] +fn test_register_content_not_writer() { + let (contract_address, _) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + + let title: felt252 = 'Unauthorized Content'; + let description: felt252 = 'This should not be registered'; + let content_type: ContentType = ContentType::Text; + let category: Category = Category::Literature; + let reader_address: ContractAddress = contract_address_const::<'reader'>(); + + // Register a user with READER role (not WRITER) + let username: felt252 = 'Reader'; + let role: Role = Role::READER; + let rank: Rank = Rank::BEGINNER; + let metadata: felt252 = 'Just a reader'; + + // Set caller address for user registration + cheat_caller_address(contract_address, reader_address, CheatSpan::Indefinite); + + // Call register_user with READER role + let user_id = dispatcher.register_user(username, role.clone(), rank.clone(), metadata); + + // Attempt to register content - this should fail + dispatcher.register_content(title, description, content_type, category); + // The test should panic for any reason +} + +#[test] +#[should_panic] +fn test_register_content_empty_title() { + let (contract_address, _) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + + // Set up content with empty title + let title: felt252 = 0; // Empty title + let description: felt252 = 'Valid description'; + let content_type: ContentType = ContentType::Text; + let category: Category = Category::Education; + let creator_address: ContractAddress = contract_address_const::<'empty_title_creator'>(); + + // Register a user with WRITER role + let username: felt252 = 'EmptyTitleCreator'; + let role: Role = Role::WRITER; + let rank: Rank = Rank::BEGINNER; + let metadata: felt252 = 'Creator testing empty title'; + + // Set caller address for user registration + cheat_caller_address(contract_address, creator_address, CheatSpan::Indefinite); + + // Call register_user + let user_id = dispatcher.register_user(username, role.clone(), rank.clone(), metadata); + + // Attempt to register content with empty title - should panic + dispatcher.register_content(title, description, content_type, category); + // Test should panic with "Title cannot be empty" +} + +#[test] +#[should_panic] +fn test_register_content_empty_description() { + let (contract_address, _) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + + // Set up content with empty description + let title: felt252 = 'Valid Title'; + let description: felt252 = 0; // Empty description + let content_type: ContentType = ContentType::Text; + let category: Category = Category::Literature; + let creator_address: ContractAddress = contract_address_const::<'empty_desc_creator'>(); + + // Register a user with WRITER role + let username: felt252 = 'EmptyDescCreator'; + let role: Role = Role::WRITER; + let rank: Rank = Rank::EXPERT; + let metadata: felt252 = 'testing empty description'; + + // Set caller address for user registration + cheat_caller_address(contract_address, creator_address, CheatSpan::Indefinite); + + // Call register_user + let user_id = dispatcher.register_user(username, role.clone(), rank.clone(), metadata); + + // Attempt to register content with empty description - should panic + dispatcher.register_content(title, description, content_type, category); + // Test should panic with "Description cannot be empty" +} + +#[test] +fn test_register_content_multiple_users() { + let (contract_address, _) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + + let mut spy = spy_events(); + + // First user setup + let first_user_address: ContractAddress = contract_address_const::<'first_creator'>(); + let first_username: felt252 = 'FirstCreator'; + let first_role: Role = Role::WRITER; + let first_rank: Rank = Rank::BEGINNER; + let first_metadata: felt252 = 'First content creator'; + + // Set caller address for first user registration + cheat_caller_address(contract_address, first_user_address, CheatSpan::Indefinite); + + // Register first user + let first_user_id = dispatcher + .register_user(first_username, first_role.clone(), first_rank.clone(), first_metadata); + + // First user registers content + let first_title: felt252 = 'First Content'; + let first_description: felt252 = 'Content by first user'; + let first_content_type: ContentType = ContentType::Text; + let first_category: Category = Category::Education; + + let first_content_id = dispatcher + .register_content(first_title, first_description, first_content_type, first_category); + + // Verify first content ID is 0 + assert(first_content_id == 0, 'First content_id should be 0'); + + // Second user setup + let second_user_address: ContractAddress = contract_address_const::<'second_creator'>(); + let second_username: felt252 = 'SecondCreator'; + let second_role: Role = Role::WRITER; + let second_rank: Rank = Rank::INTERMEDIATE; + let second_metadata: felt252 = 'Second content creator'; + + // Set caller address for second user registration + cheat_caller_address(contract_address, second_user_address, CheatSpan::Indefinite); + + // Register second user + let second_user_id = dispatcher + .register_user(second_username, second_role.clone(), second_rank.clone(), second_metadata); + + // Second user registers content + let second_title: felt252 = 'Second Content'; + let second_description: felt252 = 'Content by second user'; + let second_content_type: ContentType = ContentType::Video; + let second_category: Category = Category::Software; + + let second_content_id = dispatcher + .register_content(second_title, second_description, second_content_type, second_category); + + // Verify second content ID is incremented + assert(second_content_id == first_content_id + 1, ' content_id not incremented'); + + // Verify content creators are correctly recorded + let first_content = dispatcher.get_content(first_content_id); + let second_content = dispatcher.get_content(second_content_id); + + assert(first_content.creator == first_user_address, 'First creator mismatch'); + assert(second_content.creator == second_user_address, 'Second creator mismatch'); + + // Verify events were emitted for both content registrations + spy + .assert_emitted( + @array![ + ( + contract_address, + chain_lib::chainlib::ChainLib::ChainLib::Event::ContentRegistered( + chain_lib::chainlib::ChainLib::ChainLib::ContentRegistered { + content_id: first_content_id, creator: first_user_address + } + ) + ), + ( + contract_address, + chain_lib::chainlib::ChainLib::ChainLib::Event::ContentRegistered( + chain_lib::chainlib::ChainLib::ChainLib::ContentRegistered { + content_id: second_content_id, creator: second_user_address + } + ) + ) + ] + ); +} + +#[test] +fn test_content_metadata_retrieval() { + let (contract_address, _) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + + // Create a user with WRITER role + let creator_address: ContractAddress = contract_address_const::<'metadata_creator'>(); + let username: felt252 = 'MetadataCreator'; + let role: Role = Role::WRITER; + let rank: Rank = Rank::INTERMEDIATE; + let user_metadata: felt252 = 'Content metadata tester'; + + // Set caller address for user registration + cheat_caller_address(contract_address, creator_address, CheatSpan::Indefinite); + + // Register the user + let _user_id = dispatcher.register_user(username, role.clone(), rank.clone(), user_metadata); + + // Register different types of content + + // 1. Text content in Education category + let text_title: felt252 = 'Text Article'; + let text_description: felt252 = 'Educational text'; + let text_content_type: ContentType = ContentType::Text; + let text_category: Category = Category::Education; + + let text_content_id = dispatcher + .register_content(text_title, text_description, text_content_type, text_category); + + // 2. Image content in Art category + let image_title: felt252 = 'Art Image'; + let image_description: felt252 = 'Artistic image'; + let image_content_type: ContentType = ContentType::Image; + let image_category: Category = Category::Art; + + let image_content_id = dispatcher + .register_content(image_title, image_description, image_content_type, image_category); + + // 3. Video content in Software category + let video_title: felt252 = 'Tutorial Video'; + let video_description: felt252 = 'Software tutorial'; + let video_content_type: ContentType = ContentType::Video; + let video_category: Category = Category::Software; + + let video_content_id = dispatcher + .register_content(video_title, video_description, video_content_type, video_category); + + // Retrieve and verify all content metadata + let text_content = dispatcher.get_content(text_content_id); + let image_content = dispatcher.get_content(image_content_id); + let video_content = dispatcher.get_content(video_content_id); + + // Verify text content metadata + assert(text_content.content_id == text_content_id, 'Text ID mismatch'); + assert(text_content.title == text_title, 'Text title mismatch'); + assert(text_content.description == text_description, 'Text desc mismatch'); + assert(text_content.content_type == text_content_type, 'Text type mismatch'); + assert(text_content.category == text_category, 'Text category mismatch'); + assert(text_content.creator == creator_address, 'Text creator mismatch'); + + // Verify image content metadata + assert(image_content.content_id == image_content_id, 'Image ID mismatch'); + assert(image_content.title == image_title, 'Image title mismatch'); + assert(image_content.description == image_description, 'Image desc mismatch'); + assert(image_content.content_type == image_content_type, 'Image type mismatch'); + assert(image_content.category == image_category, 'Image category mismatch'); + assert(image_content.creator == creator_address, 'Image creator mismatch'); + + // Verify video content metadata + assert(video_content.content_id == video_content_id, 'Video ID mismatch'); + assert(video_content.title == video_title, 'Video title mismatch'); + assert(video_content.description == video_description, 'Video desc mismatch'); + assert(video_content.content_type == video_content_type, 'Video type mismatch'); + assert(video_content.category == video_category, 'Video category mismatch'); + assert(video_content.creator == creator_address, 'Video creator mismatch'); + + // Verify content IDs are sequential + assert(image_content_id == text_content_id + 1, 'Image ID not sequential'); + assert(video_content_id == image_content_id + 1, 'Video ID not sequential'); +}