From 77b4d282be9dd73dece9ffa21a96a3cf411b8ddd Mon Sep 17 00:00:00 2001 From: Anonfedora Date: Mon, 28 Jul 2025 22:47:40 +0100 Subject: [PATCH 1/2] feat: Content Update/tests: --- .tool-versions | 1 + src/chainlib/ChainLib.cairo | 359 ++++++++++++++++++++++++++- src/interfaces/IChainLib.cairo | 39 ++- tests/lib.cairo | 1 + tests/test_account_delegation.cairo | 1 - tests/test_content_update.cairo | 370 ++++++++++++++++++++++++++++ 6 files changed, 757 insertions(+), 14 deletions(-) create mode 100644 tests/test_content_update.cairo diff --git a/.tool-versions b/.tool-versions index 3acc460..dda1bd9 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ scarb 2.11.2 sstarknet-foundry 0.40.0 +starknet-foundry 0.40.0 diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index a62b810..7e8e148 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -18,23 +18,12 @@ pub mod ChainLib { AccessRule, AccessType, Payout, PayoutSchedule, PayoutStatus, Permissions, Purchase, PurchaseStatus, Rank, Receipt, ReceiptStatus, Refund, RefundRequestReason, RefundStatus, Role, Status, TokenBoundAccount, User, VerificationRequirement, VerificationType, - permission_flags, + delegation_flags, permission_flags, }; use crate::interfaces::IChainLib::IChainLib; // Define delegation-specific structures and constants - // New delegation flags - extending the existing permission_flags - pub mod delegation_flags { - // Using higher bits to avoid collision with existing permission flags - const DELEGATE_TRANSFER: u64 = 0x10000; - const DELEGATE_CONTENT: u64 = 0x20000; - const DELEGATE_ADMIN: u64 = 0x40000; - const DELEGATE_USER: u64 = 0x80000; - // Combined flag for full delegation capabilities - const FULL_DELEGATION: u64 = 0xF0000; - } - #[derive(Copy, Drop, Serde, starknet::Store, Debug)] pub struct DelegationInfo { pub delegator: ContractAddress, // The account owner who created the delegation @@ -80,6 +69,36 @@ pub mod ChainLib { pub content_type: ContentType, pub creator: ContractAddress, pub category: Category, + pub last_updated: u64, + pub version: u64, + } + + #[derive(Copy, Drop, Serde, starknet::Store, Debug)] + pub struct ContentUpdateHistory { + pub content_id: felt252, + pub version: u64, + pub updater: ContractAddress, + pub timestamp: u64, + pub update_type: ContentUpdateType, + pub previous_title: felt252, + pub previous_description: felt252, + pub previous_content_type: ContentType, + pub previous_category: Category, + pub new_title: felt252, + pub new_description: felt252, + pub new_content_type: ContentType, + pub new_category: Category, + } + + #[derive(Copy, Drop, Serde, starknet::Store, PartialEq, Debug)] + pub enum ContentUpdateType { + #[default] + Full, + Partial, + Title, + Description, + ContentType, + Category, } #[derive(Drop, Serde, starknet::Store, Clone)] @@ -232,6 +251,12 @@ pub mod ChainLib { >, // map a creator's address to a Vec of his payouts user_refunds: Map>, refund_window: u64, + // Content Update Tracking + content_update_history: Map< + (felt252, u64), ContentUpdateHistory, + >, // Maps (content_id, version) to update history + content_version_count: Map, // Maps content_id to current version count + content_update_count: Map // Maps content_id to total update count } const REFUND_WINDOW: u64 = 86400; @@ -307,6 +332,9 @@ pub mod ChainLib { PlatformFeeChanged: PlatformFeeChanged, RefundWindowChanged: RefundWindowChanged, RefundTimedOut: RefundTimedOut, + // Content Update Events + ContentUpdated: ContentUpdated, + ContentUpdateHistoryRecorded: ContentUpdateHistoryRecorded, } #[derive(Drop, starknet::Event)] @@ -546,6 +574,24 @@ pub mod ChainLib { pub timestamp: u64, } + #[derive(Drop, starknet::Event)] + pub struct ContentUpdated { + pub content_id: felt252, + pub updater: ContractAddress, + pub version: u64, + pub update_type: ContentUpdateType, + pub timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct ContentUpdateHistoryRecorded { + pub content_id: felt252, + pub version: u64, + pub updater: ContractAddress, + pub update_type: ContentUpdateType, + pub timestamp: u64, + } + #[abi(embed_v0)] impl ChainLibNetImpl of IChainLib { fn create_token_account( @@ -856,6 +902,8 @@ pub mod ChainLib { content_type: content_type, creator: creator, category: category, + last_updated: get_block_timestamp(), + version: 1, }; self.content.write(content_id, content_metadata); @@ -875,6 +923,238 @@ pub mod ChainLib { content_metadata } + /// @notice Updates existing content with ownership verification + /// @dev Only content creators or authorized users can update content + /// @param content_id The unique identifier of the content to update + /// @param title The new title (0 to keep existing) + /// @param description The new description (0 to keep existing) + /// @param content_type The new content type (None to keep existing) + /// @param category The new category (None to keep existing) + /// @return bool Returns true if the update was successful + fn update_content( + ref self: ContractState, + content_id: felt252, + title: felt252, + description: felt252, + content_type: Option, + category: Option, + ) -> bool { + // Verify content exists + let mut content = self.content.read(content_id); + assert!(content.content_id == content_id, "Content does not exist"); + + // Ownership verification using comprehensive permission check + let caller = get_caller_address(); + assert!( + self.can_update_content(content_id, caller), + "Only authorized users can update content", + ); + + // Store previous values for history tracking + let previous_title = content.title; + let previous_description = content.description; + let previous_content_type = content.content_type; + let previous_category = content.category; + + // Determine update type + let mut update_type = ContentUpdateType::Full; + let mut fields_updated: u32 = 0; + + // Update title if provided + if title != 0 { + content.title = title; + fields_updated += 1_u32; + } + + // Update description if provided + if description != 0 { + content.description = description; + fields_updated += 1_u32; + } + + // Update content type if provided + if content_type.is_some() { + content.content_type = content_type.unwrap(); + fields_updated += 1_u32; + } + + // Update category if provided + if category.is_some() { + content.category = category.unwrap(); + fields_updated += 1_u32; + } + + // Determine specific update type based on what was changed + if fields_updated == 1_u32 { + let content_type_some = content_type.is_some(); + let category_some = category.is_some(); + if title != 0 && description == 0 && !content_type_some && !category_some { + update_type = ContentUpdateType::Title; + } else if title == 0 && description != 0 && !content_type_some && !category_some { + update_type = ContentUpdateType::Description; + } else if title == 0 && description == 0 && content_type_some && !category_some { + update_type = ContentUpdateType::ContentType; + } else if title == 0 && description == 0 && !content_type_some && category_some { + update_type = ContentUpdateType::Category; + } else { + update_type = ContentUpdateType::Partial; + } + } else if fields_updated > 1_u32 { + update_type = ContentUpdateType::Partial; + } + + // Update version and timestamp + let new_version = content.version + 1; + content.version = new_version; + content.last_updated = get_block_timestamp(); + + // Store updated content + self.content.write(content_id, content); + self.creators_content.write(content.creator, content); + + // Record update history + self + ._record_content_update_history( + content_id, + new_version, + caller, + update_type, + previous_title, + previous_description, + previous_content_type, + previous_category, + content.title, + content.description, + content.content_type, + content.category, + ); + + // Update version counter + self.content_version_count.write(content_id, new_version); + let current_update_count = self.content_update_count.read(content_id); + self.content_update_count.write(content_id, current_update_count + 1); + + // Emit content updated event + self + .emit( + ContentUpdated { + content_id, + updater: caller, + version: new_version, + update_type, + timestamp: get_block_timestamp(), + }, + ); + + true + } + + /// @notice Updates only the title of existing content + /// @dev Only content creators or authorized users can update content + /// @param content_id The unique identifier of the content to update + /// @param title The new title + /// @return bool Returns true if the update was successful + fn update_content_title( + ref self: ContractState, content_id: felt252, title: felt252, + ) -> bool { + assert!(title != 0, "Title cannot be empty"); + self.update_content(content_id, title, 0, Option::None, Option::None) + } + + /// @notice Updates only the description of existing content + /// @dev Only content creators or authorized users can update content + /// @param content_id The unique identifier of the content to update + /// @param description The new description + /// @return bool Returns true if the update was successful + fn update_content_description( + ref self: ContractState, content_id: felt252, description: felt252, + ) -> bool { + assert!(description != 0, "Description cannot be empty"); + self.update_content(content_id, 0, description, Option::None, Option::None) + } + + /// @notice Updates only the content type of existing content + /// @dev Only content creators or authorized users can update content + /// @param content_id The unique identifier of the content to update + /// @param content_type The new content type + /// @return bool Returns true if the update was successful + fn update_content_type( + ref self: ContractState, content_id: felt252, content_type: ContentType, + ) -> bool { + self.update_content(content_id, 0, 0, Option::Some(content_type), Option::None) + } + + /// @notice Updates only the category of existing content + /// @dev Only content creators or authorized users can update content + /// @param content_id The unique identifier of the content to update + /// @param category The new category + /// @return bool Returns true if the update was successful + fn update_content_category( + ref self: ContractState, content_id: felt252, category: Category, + ) -> bool { + self.update_content(content_id, 0, 0, Option::None, Option::Some(category)) + } + + /// @notice Gets the update history for a specific content + /// @param content_id The unique identifier of the content + /// @param version The specific version to retrieve (0 for latest) + /// @return ContentUpdateHistory The update history record + fn get_content_update_history( + self: @ContractState, content_id: felt252, version: u64, + ) -> ContentUpdateHistory { + if version == 0 { + let latest_version = self.content_version_count.read(content_id); + return self.content_update_history.read((content_id, latest_version)); + } + self.content_update_history.read((content_id, version)) + } + + /// @notice Gets the current version of content + /// @param content_id The unique identifier of the content + /// @return u64 The current version number + fn get_content_version(self: @ContractState, content_id: felt252) -> u64 { + self.content_version_count.read(content_id) + } + + /// @notice Gets the total number of updates for content + /// @param content_id The unique identifier of the content + /// @return u64 The total number of updates + fn get_content_update_count(self: @ContractState, content_id: felt252) -> u64 { + self.content_update_count.read(content_id) + } + + /// @notice Checks if a user has permission to update content + /// @param content_id The unique identifier of the content + /// @param user The address of the user to check + /// @return bool Returns true if the user has update permission + fn can_update_content( + self: @ContractState, content_id: felt252, user: ContractAddress, + ) -> bool { + let content = self.content.read(content_id); + + // Check if user is the content creator + if content.creator == user { + return true; + } + + // Check if user is admin + if self.admin.read() == user { + return true; + } + + // Check if user has content-specific permissions + if self.has_content_permission(content_id, user, permission_flags::WRITE) { + return true; + } + + // Check if user has delegation for content updates + if self.is_delegated(content.creator, user, delegation_flags::DELEGATE_CONTENT) { + return true; + } + + false + } + /// @notice Processes the initial payment for a new subscription /// @param amount The amount to be charged for the initial payment @@ -2570,6 +2850,61 @@ pub mod ChainLib { assert(balance >= amount, payment_errors::INSUFFICIENT_BALANCE); } + /// @notice Internal function to record content update history + /// @param content_id The unique identifier of the content + /// @param version The version number + /// @param updater The address of the updater + /// @param update_type The type of update performed + /// @param previous_title The previous title + /// @param previous_description The previous description + /// @param previous_content_type The previous content type + /// @param previous_category The previous category + /// @param new_title The new title + /// @param new_description The new description + /// @param new_content_type The new content type + /// @param new_category The new category + fn _record_content_update_history( + ref self: ContractState, + content_id: felt252, + version: u64, + updater: ContractAddress, + update_type: ContentUpdateType, + previous_title: felt252, + previous_description: felt252, + previous_content_type: ContentType, + previous_category: Category, + new_title: felt252, + new_description: felt252, + new_content_type: ContentType, + new_category: Category, + ) { + let update_history = ContentUpdateHistory { + content_id, + version, + updater, + timestamp: get_block_timestamp(), + update_type, + previous_title, + previous_description, + previous_content_type, + previous_category, + new_title, + new_description, + new_content_type, + new_category, + }; + + self.content_update_history.write((content_id, version), update_history); + + // Emit history recorded event + self + .emit( + ContentUpdateHistoryRecorded { + content_id, version, updater, update_type, timestamp: get_block_timestamp(), + }, + ); + } + fn _single_payout( ref self: ContractState, recipient_address: ContractAddress, amount: u256, ) -> bool { diff --git a/src/interfaces/IChainLib.cairo b/src/interfaces/IChainLib.cairo index 5b89feb..2e19b6d 100644 --- a/src/interfaces/IChainLib.cairo +++ b/src/interfaces/IChainLib.cairo @@ -5,7 +5,8 @@ use crate::base::types::{ Role, TokenBoundAccount, User, VerificationRequirement, VerificationType, }; use crate::chainlib::ChainLib::ChainLib::{ - Category, ContentMetadata, ContentType, DelegationInfo, Payment, PlanType, Subscription, + Category, ContentMetadata, ContentType, ContentUpdateHistory, ContentUpdateType, DelegationInfo, + Payment, PlanType, Subscription, }; #[starknet::interface] @@ -75,6 +76,42 @@ pub trait IChainLib { ) -> felt252; fn get_content(ref self: TContractState, content_id: felt252) -> ContentMetadata; + // Content Update Functions + fn update_content( + ref self: TContractState, + content_id: felt252, + title: felt252, + description: felt252, + content_type: Option, + category: Option, + ) -> bool; + + fn update_content_title(ref self: TContractState, content_id: felt252, title: felt252) -> bool; + + fn update_content_description( + ref self: TContractState, content_id: felt252, description: felt252, + ) -> bool; + + fn update_content_type( + ref self: TContractState, content_id: felt252, content_type: ContentType, + ) -> bool; + + fn update_content_category( + ref self: TContractState, content_id: felt252, category: Category, + ) -> bool; + + fn get_content_update_history( + self: @TContractState, content_id: felt252, version: u64, + ) -> ContentUpdateHistory; + + fn get_content_version(self: @TContractState, content_id: felt252) -> u64; + + fn get_content_update_count(self: @TContractState, content_id: felt252) -> u64; + + fn can_update_content( + self: @TContractState, content_id: felt252, user: ContractAddress, + ) -> bool; + fn set_content_price(ref self: TContractState, content_id: felt252, price: u256); diff --git a/tests/lib.cairo b/tests/lib.cairo index dc89ff1..975d055 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -1,6 +1,7 @@ #[cfg(test)] pub mod test_ChainLib; pub mod test_account_delegation; +pub mod test_content_update; pub mod test_contentaccess; pub mod test_contentpost; pub mod test_permissions; diff --git a/tests/test_account_delegation.cairo b/tests/test_account_delegation.cairo index 30b17e3..138b674 100644 --- a/tests/test_account_delegation.cairo +++ b/tests/test_account_delegation.cairo @@ -7,7 +7,6 @@ const PERMISSION_CALL: u64 = 0x4; const PERMISSION_ADMIN: u64 = 0x8; use chain_lib::chainlib::ChainLib::ChainLib::{ DelegationCreated, DelegationExpired, DelegationInfo, DelegationRevoked, DelegationUsed, Event, - delegation_flags, }; use chain_lib::interfaces::IChainLib::{IChainLibDispatcher, IChainLibDispatcherTrait}; // use chain_lib::interfaces::IChainLib::{ diff --git a/tests/test_content_update.cairo b/tests/test_content_update.cairo new file mode 100644 index 0000000..9ea51ea --- /dev/null +++ b/tests/test_content_update.cairo @@ -0,0 +1,370 @@ +use chain_lib::base::types::{Rank, Role}; +use chain_lib::chainlib::ChainLib::ChainLib::{Category, ContentType, ContentUpdateType}; +use chain_lib::interfaces::IChainLib::{IChainLibDispatcher, IChainLibDispatcherTrait}; +use snforge_std::{ + spy_events, start_cheat_block_timestamp, start_cheat_caller_address, stop_cheat_block_timestamp, + stop_cheat_caller_address, +}; +use starknet::contract_address::contract_address_const; +use crate::test_utils::setup; + +#[test] +fn test_update_content_basic() { + let (contract_address, _admin_address, _erc20_address) = setup(); + let contract_instance = IChainLibDispatcher { contract_address }; + + // Setup addresses + let creator = contract_address_const::<'CREATOR'>(); + let content_id: felt252 = 0; + + // Set creator as caller + start_cheat_caller_address(contract_address, creator); + + // Set current time + let current_time: u64 = 1000; + start_cheat_block_timestamp(contract_address, current_time); + + // Register user as WRITER first + let username = 'Creator'; + let role = Role::WRITER; + let rank = Rank::BEGINNER; + let metadata = 'Test creator'; + + // Register user + contract_instance.register_user(username, role, rank, metadata); + + // Register content first + let title = 'Original'; + let description = 'Original Desc'; + let content_type = ContentType::Text; + let category = Category::Education; + + contract_instance.register_content(title, description, content_type, category); + + // Spy on events + let mut spy = spy_events(); + + // Update content + let new_title = 'Updated'; + let new_description = 'Updated Desc'; + let new_content_type = ContentType::Video; + let new_category = Category::Software; + + let result = contract_instance + .update_content( + content_id, + new_title, + new_description, + Option::Some(new_content_type), + Option::Some(new_category), + ); + + assert(result, 'Content update should succeed'); + + // Verify content was updated + let updated_content = contract_instance.get_content(content_id); + assert(updated_content.title == new_title, 'Title updated'); + assert(updated_content.description == new_description, 'Desc updated'); + assert(updated_content.content_type == new_content_type, 'Type updated'); + assert(updated_content.category == new_category, 'Cat updated'); + assert(updated_content.version == 2, 'Version incremented'); + assert(updated_content.last_updated >= current_time, 'Time updated'); + + // Verify event was emitted + // let expected_event = Event::ContentUpdated( + // ContentUpdated { + // content_id, + // updater: creator, + // version: 2, + // update_type: ContentUpdateType::Full, + // timestamp: get_block_timestamp(), + // }, + // ); + // spy.assert_emitted(@array![(contract_address, expected_event)]); + + stop_cheat_caller_address(creator); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +fn test_update_content_partial() { + let (contract_address, _admin_address, _erc20_address) = setup(); + let contract_instance = IChainLibDispatcher { contract_address }; + + // Setup addresses + let creator = contract_address_const::<'CREATOR'>(); + let content_id: felt252 = 0; + + // Set creator as caller + start_cheat_caller_address(contract_address, creator); + + // Set current time + let current_time: u64 = 1000; + start_cheat_block_timestamp(contract_address, current_time); + + // Register user as WRITER first + let username = 'Creator'; + let role = Role::WRITER; + let rank = Rank::BEGINNER; + let metadata = 'Test creator'; + + // Register user + contract_instance.register_user(username, role, rank, metadata); + + // Register content first + let title = 'Original'; + let description = 'Original Desc'; + let content_type = ContentType::Text; + let category = Category::Education; + + contract_instance.register_content(title, description, content_type, category); + + // Spy on events + let mut spy = spy_events(); + + // Update only title + let new_title = 'Updated'; + let result = contract_instance.update_content_title(content_id, new_title); + + assert(result, 'Title update succeed'); + + // Verify only title was updated + let updated_content = contract_instance.get_content(content_id); + assert(updated_content.title == new_title, 'Title updated'); + assert(updated_content.description == description, 'Desc unchanged'); + assert(updated_content.content_type == content_type, 'Type unchanged'); + assert(updated_content.category == category, 'Cat unchanged'); + assert(updated_content.version == 2, 'Version incremented'); + + // Verify event was emitted + // let expected_event = Event::ContentUpdated( + // ContentUpdated { + // content_id, + // updater: creator, + // version: 2, + // update_type: ContentUpdateType::Title, + // timestamp: get_block_timestamp(), + // }, + // ); + // spy.assert_emitted(@array![(contract_address, expected_event)]); + + stop_cheat_caller_address(creator); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +#[should_panic] +fn test_update_content_not_found() { + let (contract_address, _admin_address, _erc20_address) = setup(); + let contract_instance = IChainLibDispatcher { contract_address }; + + // Setup addresses + let creator = contract_address_const::<'CREATOR'>(); + let non_existent_content_id: felt252 = 999; + + // Set creator as caller + start_cheat_caller_address(contract_address, creator); + + // Try to update non-existent content + contract_instance.update_content_title(non_existent_content_id, 'New Title'); + + stop_cheat_caller_address(creator); +} + +#[test] +#[should_panic] +fn test_update_content_unauthorized() { + let (contract_address, _admin_address, _erc20_address) = setup(); + let contract_instance = IChainLibDispatcher { contract_address }; + + // Setup addresses + let creator = contract_address_const::<'CREATOR'>(); + let unauthorized_user = contract_address_const::<'UNAUTHORIZED'>(); + let content_id: felt252 = 0; + + // Set creator as caller to register content + start_cheat_caller_address(contract_address, creator); + + // Register user as WRITER first + let username = 'Creator'; + let role = Role::WRITER; + let rank = Rank::BEGINNER; + let metadata = 'Test creator'; + + // Register user + contract_instance.register_user(username, role, rank, metadata); + + // Register content + contract_instance.register_content('Title', 'Desc', ContentType::Text, Category::Education); + + // Switch to unauthorized user + start_cheat_caller_address(contract_address, unauthorized_user); + + // Try to update content as unauthorized user + contract_instance.update_content_title(content_id, 'New Title'); + + stop_cheat_caller_address(unauthorized_user); +} + +#[test] +fn test_update_content_as_admin() { + let (contract_address, admin_address, _erc20_address) = setup(); + let contract_instance = IChainLibDispatcher { contract_address }; + + // Setup addresses + let creator = contract_address_const::<'CREATOR'>(); + let content_id: felt252 = 0; + + // Set creator as caller to register content + start_cheat_caller_address(contract_address, creator); + + // Register user as WRITER first + let username = 'Creator'; + let role = Role::WRITER; + let rank = Rank::BEGINNER; + let metadata = 'Test creator'; + + // Register user + contract_instance.register_user(username, role, rank, metadata); + + // Register content + contract_instance.register_content('Title', 'Desc', ContentType::Text, Category::Education); + + // Switch to admin + start_cheat_caller_address(contract_address, admin_address); + + // Update content as admin + let result = contract_instance.update_content_title(content_id, 'Admin Updated'); + + assert(result, 'Admin can update'); + + // Verify content was updated + let updated_content = contract_instance.get_content(content_id); + assert(updated_content.title == 'Admin Updated', 'Title updated by admin'); + assert(updated_content.version == 2, 'Version incremented'); + + stop_cheat_caller_address(admin_address); +} + +#[test] +fn test_content_update_history() { + let (contract_address, _admin_address, _erc20_address) = setup(); + let contract_instance = IChainLibDispatcher { contract_address }; + + // Setup addresses + let creator = contract_address_const::<'CREATOR'>(); + let content_id: felt252 = 0; + + // Set creator as caller + start_cheat_caller_address(contract_address, creator); + + // Set current time + let current_time: u64 = 1000; + start_cheat_block_timestamp(contract_address, current_time); + + // Register user as WRITER first + let username = 'Creator'; + let role = Role::WRITER; + let rank = Rank::BEGINNER; + let metadata = 'Test creator'; + + // Register user + contract_instance.register_user(username, role, rank, metadata); + + // Register content + contract_instance.register_content('Title', 'Desc', ContentType::Text, Category::Education); + + // Update content + contract_instance.update_content_title(content_id, 'Updated'); + + // Get update history + let history = contract_instance.get_content_update_history(content_id, 2); + + // Verify history was recorded + assert(history.content_id == content_id, 'Wrong content ID'); + assert(history.version == 2, 'Wrong version'); + assert(history.updater == creator, 'Wrong updater'); + assert(history.update_type == ContentUpdateType::Title, 'Wrong type'); + assert(history.previous_title == 'Title', 'Wrong prev title'); + assert(history.new_title == 'Updated', 'Wrong new title'); + + stop_cheat_caller_address(creator); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +fn test_content_version_tracking() { + let (contract_address, _admin_address, _erc20_address) = setup(); + let contract_instance = IChainLibDispatcher { contract_address }; + + // Setup addresses + let creator = contract_address_const::<'CREATOR'>(); + let content_id: felt252 = 0; + + // Set creator as caller + start_cheat_caller_address(contract_address, creator); + + // Register user as WRITER first + let username = 'Creator'; + let role = Role::WRITER; + let rank = Rank::BEGINNER; + let metadata = 'Test creator'; + + // Register user + contract_instance.register_user(username, role, rank, metadata); + + // Register content + contract_instance.register_content('Title', 'Desc', ContentType::Text, Category::Education); + + // Verify initial version after registration + let initial_content = contract_instance.get_content(content_id); + assert(initial_content.version == 1, 'Initial version 1'); + assert(contract_instance.get_content_update_count(content_id) == 0, 'Initial count 0'); + + // Update content multiple times + contract_instance.update_content_title(content_id, 'Title 2'); + contract_instance.update_content_description(content_id, 'Desc 2'); + contract_instance.update_content_type(content_id, ContentType::Video); + + // Verify version tracking + assert(contract_instance.get_content_version(content_id) == 4, 'Version 4 after 3 updates'); + assert(contract_instance.get_content_update_count(content_id) == 3, 'Count 3'); + + stop_cheat_caller_address(creator); +} + +#[test] +fn test_can_update_content_permissions() { + let (contract_address, admin_address, _erc20_address) = setup(); + let contract_instance = IChainLibDispatcher { contract_address }; + + // Setup addresses + let creator = contract_address_const::<'CREATOR'>(); + let unauthorized_user = contract_address_const::<'UNAUTHORIZED'>(); + let content_id: felt252 = 0; + + // Set creator as caller to register content + start_cheat_caller_address(contract_address, creator); + + // Register user as WRITER first + let username = 'Creator'; + let role = Role::WRITER; + let rank = Rank::BEGINNER; + let metadata = 'Test creator'; + + // Register user + contract_instance.register_user(username, role, rank, metadata); + + // Register content + contract_instance.register_content('Title', 'Desc', ContentType::Text, Category::Education); + + // Test permissions + assert(contract_instance.can_update_content(content_id, creator), 'Creator has permission'); + assert(contract_instance.can_update_content(content_id, admin_address), 'Admin has permission'); + assert( + !contract_instance.can_update_content(content_id, unauthorized_user), + 'Unauthorized no permission', + ); + + stop_cheat_caller_address(creator); +} From 78ba5c83f94aa2d0ba05d2ed5e0b1ff87c1bbdec Mon Sep 17 00:00:00 2001 From: Anonfedora Date: Tue, 29 Jul 2025 18:44:20 +0100 Subject: [PATCH 2/2] fix: .tools-versions --- .tool-versions | 1 - 1 file changed, 1 deletion(-) diff --git a/.tool-versions b/.tool-versions index dda1bd9..29753a3 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,2 @@ scarb 2.11.2 -sstarknet-foundry 0.40.0 starknet-foundry 0.40.0