diff --git a/.github/workflows/contracts-ci.yml b/.github/workflows/contracts-ci.yml index 6a6ae56..e350012 100644 --- a/.github/workflows/contracts-ci.yml +++ b/.github/workflows/contracts-ci.yml @@ -20,14 +20,16 @@ jobs: - name: Install asdf uses: asdf-vm/actions/setup@v2 - - name: Install plugins + - name: Install Dojo using dojoup + run: | + curl -L https://install.dojoengine.org | bash + . "$HOME/.config/.dojo/env" + dojoup install 1.5.0 + echo "$HOME/.config/.dojo/bin" >> $GITHUB_PATH + shell: bash + + - name: Install Starknet Foundry run: | - asdf plugin add scarb - asdf install scarb 2.10.1 - asdf global scarb 2.10.1 - asdf plugin add dojo https://github.com/dojoengine/asdf-dojo - asdf install dojo 1.5.0 - asdf global dojo 1.5.0 asdf plugin add starknet-foundry asdf install starknet-foundry 0.35.0 asdf global starknet-foundry 0.35.0 diff --git a/src/helpers/security.cairo b/src/helpers/security.cairo new file mode 100644 index 0000000..5f416db --- /dev/null +++ b/src/helpers/security.cairo @@ -0,0 +1,273 @@ +use starknet::{ContractAddress, get_caller_address, get_block_timestamp}; +use dojo::model::ModelStorage; +use dojo::event::EventStorage; +use dojo::world::WorldStorage; +use coa::models::core::Contract; +use coa::models::security::{ + RateLimit, SecurityConfig, AdminRole, PlayerSecurityStatus, SecurityEvent, RateLimitExceeded, + ContractPaused, ContractUnpaused, +}; +use coa::models::session::SessionKey; +use core::num::traits::Zero; +use core::poseidon::poseidon_hash_span; + +// Security constants +pub const SUPER_ADMIN: felt252 = 'SUPER_ADMIN'; +pub const GAME_ADMIN: felt252 = 'GAME_ADMIN'; +pub const MODERATOR: felt252 = 'MODERATOR'; + +// Operation types as numbers for rate limiting +pub const CREATE_SESSION_OP: u32 = 1; +pub const SPAWN_ITEMS_OP: u32 = 2; +pub const ADMIN_ACTION_OP: u32 = 3; + +pub const SECURITY_CONFIG_ID: felt252 = 'SECURITY_CONFIG'; +pub const COA_CONTRACTS: felt252 = 'COA_CONTRACTS'; + +// Basic admin validation function (simplified) +pub fn validate_admin_access(world: WorldStorage, _required_role: felt252) { + let caller = get_caller_address(); + + // Validate caller is not zero address + assert(!caller.is_zero(), 'ZERO_ADDRESS'); + + // Read contract state + let contract: Contract = world.read_model(COA_CONTRACTS); + + // Validate contract state + assert(!contract.admin.is_zero(), 'INVALID_CONTRACT_STATE'); + assert(!contract.paused, 'CONTRACT_PAUSED'); + + // Basic admin check (simplified for now) + assert(caller == contract.admin, 'INSUFFICIENT_PERMISSIONS'); +} + +// Full admin validation function +pub fn validate_admin_access_full(world: WorldStorage, required_role: felt252) { + let caller = get_caller_address(); + + // Validate caller is not zero address + assert(!caller.is_zero(), 'ZERO_ADDRESS'); + + // Read contract state + let contract: Contract = world.read_model(COA_CONTRACTS); + + // Validate contract state + assert(!contract.admin.is_zero(), 'INVALID_CONTRACT_STATE'); + assert(!contract.paused, 'CONTRACT_PAUSED'); + + // Check if caller is super admin (contract admin) + if caller == contract.admin { + return; + } + + // Check role-based access + let admin_role: AdminRole = world.read_model(caller); + assert(admin_role.is_active, 'INSUFFICIENT_PERMISSIONS'); + + // Validate role hierarchy using if-else + if required_role == SUPER_ADMIN { + assert(caller == contract.admin, 'SUPER_ADMIN_REQUIRED'); + } else if required_role == GAME_ADMIN { + assert( + caller == contract.admin + || admin_role.role_type == GAME_ADMIN + || admin_role.role_type == SUPER_ADMIN, + 'GAME_ADMIN_REQUIRED', + ); + } else if required_role == MODERATOR { + assert( + caller == contract.admin + || admin_role.role_type == SUPER_ADMIN + || admin_role.role_type == GAME_ADMIN + || admin_role.role_type == MODERATOR, + 'MODERATOR_REQUIRED', + ); + } else { + assert(false, 'INVALID_ROLE'); + } +} + +pub fn validate_player_access(world: WorldStorage, player_id: ContractAddress) { + let caller = get_caller_address(); + + // Validate caller is not zero address + assert(!caller.is_zero(), 'ZERO_ADDRESS'); + + // Validate player ID matches caller (unless admin) + let contract: Contract = world.read_model(COA_CONTRACTS); + if caller != contract.admin { + assert(caller == player_id, 'UNAUTHORIZED_PLAYER'); + } + + // Check if player is banned + let security_status: PlayerSecurityStatus = world.read_model(player_id); + if security_status.is_banned { + let current_time = get_block_timestamp(); + if security_status.ban_expires_at > current_time { + assert(false, 'PLAYER_BANNED'); + } + } +} + +pub fn check_rate_limit( + mut world: WorldStorage, user: ContractAddress, operation_type: u32, +) -> bool { + let current_time = get_block_timestamp(); + let time_window = current_time / 3600; // 1 hour windows + let operation_felt: felt252 = operation_type.into(); + let rate_limit_key = (user, operation_felt, time_window); + + let mut rate_limit: RateLimit = world.read_model(rate_limit_key); + let security_config: SecurityConfig = world.read_model(SECURITY_CONFIG_ID); + + // Use if-else for operation type checking + let limit = if operation_type == CREATE_SESSION_OP { + security_config.max_sessions_per_hour + } else if operation_type == SPAWN_ITEMS_OP { + security_config.max_spawns_per_hour + } else if operation_type == ADMIN_ACTION_OP { + 50 // Default admin action limit + } else { + 100 // Default limit + }; + + if rate_limit.count >= limit { + // Emit rate limit exceeded event + let event = RateLimitExceeded { + user, + operation: operation_felt, + current_count: rate_limit.count, + limit, + timestamp: current_time, + }; + world.emit_event(@event); + + // Log security event + let security_event = SecurityEvent { + event_type: 'RATE_LIMIT_EXCEEDED', + user, + timestamp: current_time, + details: operation_felt, + }; + world.emit_event(@security_event); + + return false; + } + + rate_limit.count += 1; + world.write_model(@rate_limit); + true +} + +pub fn sanitize_input(input: felt252) -> felt252 { + // Basic input sanitization - in a real implementation, + // you might want to check for malicious patterns + input +} + +pub fn validate_faction(faction: felt252) -> bool { + faction == 'CHAOS_MERCENARIES' || faction == 'SUPREME_LAW' || faction == 'REBEL_TECHNOMANCERS' +} + +pub fn validate_session_duration(duration: u64) -> bool { + duration >= 3600 && duration <= 86400 // 1 hour to 24 hours +} + +pub fn generate_secure_session_id(player: ContractAddress) -> felt252 { + // Use multiple sources of entropy for security + let tx_hash: felt252 = starknet::get_tx_info().unbox().transaction_hash; + let block_timestamp: felt252 = get_block_timestamp().into(); + let player_address: felt252 = player.into(); + + // Combine entropy sources + let mut hash_data = array![tx_hash, block_timestamp, player_address]; + poseidon_hash_span(hash_data.span()) +} + +pub fn validate_contract_not_paused(world: WorldStorage) { + let contract: Contract = world.read_model(COA_CONTRACTS); + assert(!contract.paused, 'CONTRACT_PAUSED'); +} + +pub fn log_security_event( + mut world: WorldStorage, event_type: felt252, user: ContractAddress, details: felt252, +) { + let event = SecurityEvent { event_type, user, timestamp: get_block_timestamp(), details }; + world.emit_event(@event); +} + +// Emergency functions +pub fn pause_contract(mut world: WorldStorage, reason: felt252) { + validate_admin_access(world, SUPER_ADMIN); + + let mut contract: Contract = world.read_model(COA_CONTRACTS); + contract.paused = true; + world.write_model(@contract); + + let event = ContractPaused { + paused_by: get_caller_address(), timestamp: get_block_timestamp(), reason, + }; + world.emit_event(@event); +} + +pub fn unpause_contract(mut world: WorldStorage) { + validate_admin_access(world, SUPER_ADMIN); + + let mut contract: Contract = world.read_model(COA_CONTRACTS); + contract.paused = false; + world.write_model(@contract); + + let event = ContractUnpaused { + unpaused_by: get_caller_address(), timestamp: get_block_timestamp(), + }; + world.emit_event(@event); +} + +// Session security helpers +pub fn create_secure_session( + mut world: WorldStorage, session_duration: u64, max_transactions: u32, +) -> felt252 { + let caller = get_caller_address(); + let current_time = get_block_timestamp(); + + // Validate inputs + assert(validate_session_duration(session_duration), 'INVALID_DURATION'); + assert(max_transactions > 0 && max_transactions <= 1000, 'INVALID_TRANSACTIONS'); + + // Check rate limiting + assert(check_rate_limit(world, caller, CREATE_SESSION_OP), 'RATE_LIMIT_EXCEEDED'); + + // Generate secure session ID + let session_id = generate_secure_session_id(caller); + + // Create session with security measures + let session_key = SessionKey { + session_id, + player_address: caller, + session_key_address: caller, + created_at: current_time, + expires_at: current_time + session_duration, + last_used: current_time, + status: 0, // Active + max_transactions, + used_transactions: 0, + is_valid: true, + }; + + world.write_model(@session_key); + session_id +} + +// Input validation helpers +pub fn validate_item_id(item_id: u256) -> bool { + item_id > 0 +} + +pub fn validate_quantity(quantity: u256) -> bool { + quantity > 0 && quantity <= 1000000 // Reasonable upper limit +} + +pub fn validate_address_not_zero(address: ContractAddress) -> bool { + !address.is_zero() +} diff --git a/src/lib.cairo b/src/lib.cairo index 912bac6..98fa133 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -30,6 +30,7 @@ pub mod models { pub mod pet_stats; pub mod tournament; pub mod session; + pub mod security; pub mod weapon { pub mod blunt; pub mod bow; @@ -65,6 +66,7 @@ pub mod helpers { pub mod gear; pub mod body; pub mod session_validation; + pub mod security; } pub mod types { @@ -87,6 +89,7 @@ pub mod test { pub mod upgrade_gear_test; pub mod gear_read_test; pub mod model_test_player; + pub mod security_test; } pub mod traits { diff --git a/src/models/security.cairo b/src/models/security.cairo new file mode 100644 index 0000000..6bae3d5 --- /dev/null +++ b/src/models/security.cairo @@ -0,0 +1,131 @@ +use starknet::ContractAddress; +use core::num::traits::Zero; + +// Security models for access control and rate limiting +#[dojo::model] +#[derive(Drop, Copy, Serde, Default)] +pub struct RateLimit { + #[key] + pub user: ContractAddress, + #[key] + pub operation: felt252, + #[key] + pub time_window: u64, // Hour-based window + pub count: u32, +} + +#[dojo::model] +#[derive(Drop, Copy, Serde, Default)] +pub struct SecurityConfig { + #[key] + pub id: felt252, + pub max_sessions_per_hour: u32, + pub max_spawns_per_hour: u32, + pub max_transactions_per_session: u32, + pub session_renewal_threshold: u64, // seconds + pub emergency_pause_enabled: bool, +} + +#[dojo::model] +#[derive(Drop, Copy, Serde, Default)] +pub struct AdminRole { + #[key] + pub admin: ContractAddress, + pub role_type: felt252, // 'SUPER_ADMIN', 'GAME_ADMIN', 'MODERATOR' + pub granted_by: ContractAddress, + pub granted_at: u64, + pub is_active: bool, +} + +#[dojo::model] +#[derive(Drop, Copy, Serde, Default)] +pub struct PlayerSecurityStatus { + #[key] + pub player: ContractAddress, + pub is_banned: bool, + pub ban_reason: felt252, + pub ban_expires_at: u64, + pub warning_count: u32, + pub last_violation: u64, +} + +// Security Events +#[derive(Drop, Copy, Serde)] +#[dojo::event] +pub struct SecurityEvent { + #[key] + pub event_type: felt252, // 'UNAUTHORIZED_ACCESS', 'RATE_LIMIT_EXCEEDED', etc. + #[key] + pub user: ContractAddress, + pub timestamp: u64, + pub details: felt252, +} + +#[derive(Drop, Copy, Serde)] +#[dojo::event] +pub struct ContractPaused { + #[key] + pub paused_by: ContractAddress, + pub timestamp: u64, + pub reason: felt252, +} + +#[derive(Drop, Copy, Serde)] +#[dojo::event] +pub struct ContractUnpaused { + #[key] + pub unpaused_by: ContractAddress, + pub timestamp: u64, +} + +#[derive(Drop, Copy, Serde)] +#[dojo::event] +pub struct EmergencyWithdraw { + #[key] + pub token: ContractAddress, + #[key] + pub withdrawn_by: ContractAddress, + pub amount: u256, + pub timestamp: u64, +} + +#[derive(Drop, Copy, Serde)] +#[dojo::event] +pub struct RateLimitExceeded { + #[key] + pub user: ContractAddress, + #[key] + pub operation: felt252, + pub current_count: u32, + pub limit: u32, + pub timestamp: u64, +} + +#[derive(Drop, Copy, Serde)] +#[dojo::event] +pub struct AdminRoleGranted { + #[key] + pub admin: ContractAddress, + #[key] + pub granted_by: ContractAddress, + pub role_type: felt252, + pub timestamp: u64, +} + +#[derive(Drop, Copy, Serde)] +#[dojo::event] +pub struct AdminRoleRevoked { + #[key] + pub admin: ContractAddress, + #[key] + pub revoked_by: ContractAddress, + pub role_type: felt252, + pub timestamp: u64, +} + +pub impl ContractAddressDefault of Default { + #[inline(always)] + fn default() -> ContractAddress { + Zero::zero() + } +} diff --git a/src/systems/core.cairo b/src/systems/core.cairo index 775f501..68adc32 100644 --- a/src/systems/core.cairo +++ b/src/systems/core.cairo @@ -4,6 +4,7 @@ /// Spawn tournamemnts and side quests here, if necessary. use coa::models::gear::{Gear, GearDetails}; + #[starknet::interface] pub trait ICore { fn spawn_items(ref self: TContractState, gear_details: Array); @@ -19,6 +20,8 @@ pub trait ICore { fn purchase_credits(ref self: TContractState); fn random_gear_generator(ref self: TContractState) -> Gear; fn pick_items(ref self: TContractState, item_ids: Array) -> Array; + fn pause_contract(ref self: TContractState, reason: felt252); + fn unpause_contract(ref self: TContractState); } #[dojo::contract] @@ -28,6 +31,7 @@ pub mod CoreActions { use dojo::model::ModelStorage; use crate::models::core::{Contract, Operator, GearSpawned, ItemPicked}; use crate::models::gear::*; + use crate::systems::gear::GearActions::GearActionsImpl; use core::array::ArrayTrait; use crate::erc1155::erc1155::IERC1155MintableDispatcher; @@ -38,6 +42,7 @@ pub mod CoreActions { use coa::helpers::gear::{parse_id, random_geartype, get_max_upgrade_level, get_min_xp_needed}; use coa::models::player::{Player, PlayerTrait}; use core::traits::Into; + use core::num::traits::Zero; const GEAR: felt252 = 'GEAR'; const COA_CONTRACTS: felt252 = 'COA_CONTRACTS'; @@ -53,6 +58,11 @@ pub mod CoreActions { ) { let mut world = self.world(@"coa_contracts"); + // Security validation for initialization + assert(!admin.is_zero(), 'INVALID_ADMIN_ADDRESS'); + assert(!erc1155.is_zero(), 'INVALID_ERC1155_ADDRESS'); + assert(!warehouse.is_zero(), 'INVALID_WAREHOUSE_ADDRESS'); + // Initialize admin let operator = Operator { id: admin, is_operator: true }; world.write_model(@operator); @@ -69,17 +79,42 @@ pub mod CoreActions { warehouse, }; world.write_model(@contract); + + // Initialize security configuration + let security_config = coa::models::security::SecurityConfig { + id: coa::helpers::security::SECURITY_CONFIG_ID, + max_sessions_per_hour: 5, + max_spawns_per_hour: 10, + max_transactions_per_session: 1000, + session_renewal_threshold: 300, // 5 minutes + emergency_pause_enabled: true, + }; + world.write_model(@security_config); } #[abi(embed_v0)] pub impl CoreActionsImpl of super::ICore { //@ryzen-xp, @truthixify fn spawn_items(ref self: ContractState, gear_details: Array) { - let caller = get_caller_address(); let mut world = self.world_default(); - let contract: Contract = world.read_model(COA_CONTRACTS); - assert(caller == contract.admin, 'Only admin can spawn items'); + // Comprehensive security validation + coa::helpers::security::validate_admin_access( + world, coa::helpers::security::GAME_ADMIN, + ); + coa::helpers::security::validate_contract_not_paused(world); + + let caller = get_caller_address(); + + // Rate limiting check + assert( + coa::helpers::security::check_rate_limit( + world, caller, coa::helpers::security::SPAWN_ITEMS_OP, + ), + 'RATE_LIMIT_EXCEEDED', + ); + + let contract: Contract = world.read_model(COA_CONTRACTS); let erc1155_dispatcher = IERC1155MintableDispatcher { contract_address: contract.erc1155, @@ -96,7 +131,11 @@ pub mod CoreActions { let details = *gear_details.at(i); + // Input validation and sanitization assert(details.validate(), 'Invalid gear details'); + assert(details.total_count > 0, 'INVALID_TOTAL_COUNT'); + assert(details.total_count <= 1000000, 'COUNT_TOO_HIGH'); + assert(details.max_upgrade_level <= 100, 'UPGRADE_LEVEL_TOO_HIGH'); // Generate ID once and reuse let item_id = self.generate_incremental_ids(details.gear_type.into()); @@ -140,13 +179,47 @@ pub mod CoreActions { fn join_tournament(ref self: ContractState) {} fn purchase_credits(ref self: ContractState) {} + // Emergency functions for admin + fn pause_contract(ref self: ContractState, reason: felt252) { + let mut world = self.world_default(); + coa::helpers::security::validate_admin_access( + world, coa::helpers::security::SUPER_ADMIN, + ); + + let mut contract: Contract = world.read_model(COA_CONTRACTS); + contract.paused = true; + world.write_model(@contract); + + coa::helpers::security::log_security_event( + world, 'CONTRACT_PAUSED', get_caller_address(), reason, + ); + } + + fn unpause_contract(ref self: ContractState) { + let mut world = self.world_default(); + coa::helpers::security::validate_admin_access( + world, coa::helpers::security::SUPER_ADMIN, + ); + + let mut contract: Contract = world.read_model(COA_CONTRACTS); + contract.paused = false; + world.write_model(@contract); + + coa::helpers::security::log_security_event( + world, 'CONTRACT_UNPAUSED', get_caller_address(), 0, + ); + } + //@ryzen-xp // random gear item genrator fn random_gear_generator(ref self: ContractState) -> Gear { - let caller = get_caller_address(); let mut world = self.world_default(); - let contract: Contract = world.read_model(COA_CONTRACTS); - assert(caller == contract.admin, 'Only admin can spawn items'); + + // Security validation + coa::helpers::security::validate_admin_access( + world, coa::helpers::security::GAME_ADMIN, + ); + coa::helpers::security::validate_contract_not_paused(world); let gear_type = random_geartype(); let item_type: felt252 = gear_type.into(); @@ -176,6 +249,14 @@ pub mod CoreActions { let mut world = self.world_default(); let caller = get_caller_address(); + // Security validation + coa::helpers::security::validate_contract_not_paused(world); + coa::helpers::security::validate_player_access(world, caller); + + // Validate input + assert(item_ids.len() > 0, 'NO_ITEMS_PROVIDED'); + assert(item_ids.len() <= 50, 'TOO_MANY_ITEMS'); // Prevent spam + // Cache contract + dispatcher let contract: Contract = world.read_model(COA_CONTRACTS); let erc1155 = IERC1155Dispatcher { contract_address: contract.erc1155 }; diff --git a/src/systems/player.cairo b/src/systems/player.cairo index 3119458..013b3c0 100644 --- a/src/systems/player.cairo +++ b/src/systems/player.cairo @@ -87,6 +87,14 @@ pub mod PlayerActions { let mut world = self.world_default(); let caller = get_caller_address(); + + // Security validation + coa::helpers::security::validate_contract_not_paused(world); + coa::helpers::security::validate_player_access(world, caller); + + // Validate faction input + assert(coa::helpers::security::validate_faction(faction), 'INVALID_FACTION'); + let mut player: Player = world.read_model(caller); if player.max_hp == 0 { @@ -133,8 +141,24 @@ pub mod PlayerActions { // get the player let player: Player = world.read_model(caller); - // Validate input arrays have same length + // Input validation assert(target.len() == target_types.len(), 'Target arrays length mismatch'); + assert(target.len() > 0, 'NO_TARGETS_PROVIDED'); + assert(target.len() <= 20, 'TOO_MANY_TARGETS'); // Prevent spam attacks + + // Validate target types + let mut i = 0; + loop { + if i >= target_types.len() { + break; + } + let target_type = *target_types.at(i); + assert( + target_type == TARGET_LIVING || target_type == TARGET_OBJECT, + 'INVALID_TARGET_TYPE', + ); + i += 1; + }; let mut target_index = 0; @@ -722,10 +746,15 @@ pub mod PlayerActions { } fn get_erc1155_address(self: @ContractState) -> ContractAddress { - // In a real implementation, this would be stored in the contract state - // For now, we return a placeholder address - // This should be replaced with the actual ERC1155 contract address - starknet::contract_address_const::<0x0>() + // Read ERC1155 address from contract configuration + let world = self.world_default(); + let contract: coa::models::core::Contract = world + .read_model(coa::helpers::security::COA_CONTRACTS); + + // Validate contract state + assert(!contract.erc1155.is_zero(), 'ERC1155_NOT_CONFIGURED'); + + contract.erc1155 } fn get_game_object_ids(self: @ContractState) -> Array { diff --git a/src/systems/session.cairo b/src/systems/session.cairo index 705f5d7..5a56867 100644 --- a/src/systems/session.cairo +++ b/src/systems/session.cairo @@ -4,7 +4,6 @@ pub mod SessionActions { use coa::models::session::{SessionKey, SessionKeyCreated}; use dojo::model::ModelStorage; use dojo::event::EventStorage; - use core::poseidon::poseidon_hash_span; // Constants for session management const DEFAULT_SESSION_DURATION: u64 = 21600; // 6 hours in seconds @@ -19,22 +18,29 @@ pub mod SessionActions { ) -> felt252 { let player = get_caller_address(); let current_time = get_block_timestamp(); + let mut world = self.world_default(); + + // Security validations + coa::helpers::security::validate_contract_not_paused(world); + coa::helpers::security::validate_player_access(world, player); // Validate session duration - assert(session_duration >= MIN_SESSION_DURATION, 'DURATION_TOO_SHORT'); - assert(session_duration <= MAX_SESSION_DURATION, 'DURATION_TOO_LONG'); + assert( + coa::helpers::security::validate_session_duration(session_duration), 'INVALID_DURATION', + ); assert(max_transactions > 0, 'INVALID_MAX_TRANSACTIONS'); assert(max_transactions <= MAX_TRANSACTIONS_PER_SESSION, 'TOO_MANY_TRANSACTIONS'); - // Check session limits before creating new session - // For now, we'll implement a simple check - in a real implementation, - // you would need to iterate through all sessions for this player - // This is a placeholder for the session limit validation - // TODO: Implement proper session counting mechanism + // Rate limiting check + assert( + coa::helpers::security::check_rate_limit( + world, player, coa::helpers::security::CREATE_SESSION_OP, + ), + 'RATE_LIMIT_EXCEEDED', + ); - // Generate unique session ID using Poseidon hash to avoid collisions - let mut hash_data = array![player.into(), current_time.into()]; - let session_id = poseidon_hash_span(hash_data.span()); + // Generate secure session ID + let session_id = coa::helpers::security::generate_secure_session_id(player); // Create session key model let session_key = SessionKey { diff --git a/src/test/security_test.cairo b/src/test/security_test.cairo new file mode 100644 index 0000000..2f417c5 --- /dev/null +++ b/src/test/security_test.cairo @@ -0,0 +1,43 @@ +#[cfg(test)] +mod security_tests { + use starknet::{ContractAddress, contract_address_const}; + use dojo::model::{ModelStorage, ModelValueStorage}; + use dojo::world::WorldStorageTrait; + use coa::models::core::Contract; + use coa::models::security::{SecurityConfig, RateLimit}; + use coa::helpers::security::{ + validate_admin_access, check_rate_limit, validate_session_duration, sanitize_input, + validate_faction, COA_CONTRACTS, SECURITY_CONFIG_ID, CREATE_SESSION_OP, + }; + + #[test] + fn test_validate_session_duration() { + // Valid durations + assert(validate_session_duration(3600), 'Should accept 1 hour'); + assert(validate_session_duration(86400), 'Should accept 24 hours'); + + // Invalid durations + assert(!validate_session_duration(3599), 'Should reject < 1 hour'); + assert(!validate_session_duration(86401), 'Should reject > 24 hours'); + } + + #[test] + fn test_validate_faction() { + // Valid factions + assert(validate_faction('CHAOS_MERCENARIES'), 'Should accept CHAOS_MERCENARIES'); + assert(validate_faction('SUPREME_LAW'), 'Should accept SUPREME_LAW'); + assert!(validate_faction('REBEL_TECHNOMANCERS'), "Should accept REBEL_TECHNOMANCERS"); + + // Invalid faction + assert(!validate_faction('INVALID_FACTION'), 'Should reject invalid faction'); + } + + #[test] + fn test_input_sanitization() { + let input = 'test_input'; + let sanitized = sanitize_input(input); + + // Basic test - in a real implementation this would do more + assert(sanitized == input, 'Input should be sanitized'); + } +}