Refactoring Checkpoint: (WIP)
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
//! Benchmarking setup for pezpallet-perwerde
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::{Pallet as Perwerde, *};
|
||||
use pezframe_benchmarking::v2::*;
|
||||
use pezframe_support::{pezpallet_prelude::Get, BoundedVec};
|
||||
use pezframe_system::RawOrigin;
|
||||
extern crate alloc;
|
||||
use alloc::vec;
|
||||
|
||||
const SEED: u32 = 0;
|
||||
|
||||
// Helper function to create BoundedVec in benchmarks
|
||||
fn create_bounded_vec<L: Get<u32>>(s: &[u8]) -> BoundedVec<u8, L> {
|
||||
s.to_vec().try_into().unwrap()
|
||||
}
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn create_course() {
|
||||
let name: BoundedVec<u8, T::MaxCourseNameLength> =
|
||||
create_bounded_vec(b"Bizinikiwi training");
|
||||
let description: BoundedVec<u8, T::MaxCourseDescLength> =
|
||||
create_bounded_vec(b"This training covers Bizinikiwi basics.");
|
||||
let content_link: BoundedVec<u8, T::MaxCourseLinkLength> =
|
||||
create_bounded_vec(b"http://example.com");
|
||||
|
||||
// In benchmark environment, AdminOrigin is bypassed
|
||||
// We use Root origin which will satisfy the origin check
|
||||
|
||||
#[extrinsic_call]
|
||||
create_course(RawOrigin::Root, name.clone(), description.clone(), content_link.clone());
|
||||
|
||||
assert!(Courses::<T>::get(0).is_some());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn enroll() {
|
||||
let student: T::AccountId = whitelisted_caller();
|
||||
let course_id = 0;
|
||||
|
||||
// Setup: Create a course first using root
|
||||
Perwerde::<T>::create_course(
|
||||
RawOrigin::Root.into(),
|
||||
create_bounded_vec(b"Benchmark Course"),
|
||||
create_bounded_vec(b"Description"),
|
||||
create_bounded_vec(b"Link"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
#[extrinsic_call]
|
||||
enroll(RawOrigin::Signed(student.clone()), course_id);
|
||||
|
||||
assert!(Enrollments::<T>::get((student, course_id)).is_some());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn complete_course() {
|
||||
let student: T::AccountId = whitelisted_caller();
|
||||
let course_id = 0;
|
||||
let points = 10;
|
||||
|
||||
// Setup: Create course and enroll student
|
||||
// Root creates the course via AdminOrigin
|
||||
Perwerde::<T>::create_course(
|
||||
RawOrigin::Root.into(),
|
||||
create_bounded_vec(b"Benchmark Course"),
|
||||
create_bounded_vec(b"Description"),
|
||||
create_bounded_vec(b"Link"),
|
||||
)
|
||||
.unwrap();
|
||||
Perwerde::<T>::enroll(RawOrigin::Signed(student.clone()).into(), course_id).unwrap();
|
||||
|
||||
// Get the actual owner from the created course
|
||||
let course = Courses::<T>::get(course_id).unwrap();
|
||||
let owner = course.owner;
|
||||
|
||||
// complete_course requires the owner to sign, not root
|
||||
#[extrinsic_call]
|
||||
complete_course(RawOrigin::Signed(owner), student.clone(), course_id, points);
|
||||
|
||||
let enrollment = Enrollments::<T>::get((student, course_id)).unwrap();
|
||||
assert!(enrollment.completed_at.is_some());
|
||||
assert_eq!(enrollment.points_earned, points);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn archive_course() {
|
||||
let course_id = 0;
|
||||
|
||||
// Setup: Create course first
|
||||
Perwerde::<T>::create_course(
|
||||
RawOrigin::Root.into(),
|
||||
create_bounded_vec(b"Benchmark Course"),
|
||||
create_bounded_vec(b"Description"),
|
||||
create_bounded_vec(b"Link"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// archive_course requires AdminOrigin (which is Root in our config)
|
||||
// The AdminOrigin::try_origin for Root returns the admin account (Alice)
|
||||
// which matches the course owner from create_course
|
||||
#[extrinsic_call]
|
||||
archive_course(RawOrigin::Root, course_id);
|
||||
|
||||
let course = Courses::<T>::get(course_id).unwrap();
|
||||
assert_eq!(course.status, CourseStatus::Archived);
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(Perwerde, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
//! # Perwerde (Education) Pallet
|
||||
//!
|
||||
//! A pallet for managing educational courses, student enrollments, and achievement tracking.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! The Perwerde pallet implements an on-chain educational platform where:
|
||||
//! - Educators create and manage courses with IPFS-linked content
|
||||
//! - Students enroll in courses and track their progress
|
||||
//! - Course completion earns points that contribute to trust scores
|
||||
//! - Educational achievements are permanently recorded on-chain
|
||||
//!
|
||||
//! ## Core Features
|
||||
//!
|
||||
//! ### Course Management
|
||||
//! - Admins create courses with name, description, and content links (IPFS)
|
||||
//! - Courses can be active or archived
|
||||
//! - Each course has a unique ID and owner
|
||||
//! - Course metadata is immutable after creation
|
||||
//!
|
||||
//! ### Student Enrollment
|
||||
//! - Students enroll in active courses
|
||||
//! - One enrollment per student per course
|
||||
//! - Enrollment history tracked with block numbers
|
||||
//! - Students can be enrolled in multiple courses simultaneously
|
||||
//!
|
||||
//! ### Completion & Points
|
||||
//! - Course owners mark student completions
|
||||
//! - Points awarded upon completion
|
||||
//! - Points contribute to Perwerde score for trust calculation
|
||||
//! - Completion timestamps recorded permanently
|
||||
//!
|
||||
//! ## Perwerde Score System
|
||||
//!
|
||||
//! The Perwerde score is derived from total education points:
|
||||
//! - Each completed course awards points
|
||||
//! - Points accumulate over time
|
||||
//! - Score used by `pezpallet-trust` for composite trust calculation
|
||||
//! - Higher education achievement improves ecosystem standing
|
||||
//!
|
||||
//! ## Interface
|
||||
//!
|
||||
//! ### Extrinsics
|
||||
//!
|
||||
//! - `create_course(name, description, content_link)` - Create new educational course (admin)
|
||||
//! - `enroll_student(course_id)` - Enroll in an active course (user)
|
||||
//! - `mark_course_completed(student, course_id, points)` - Award completion points (course owner)
|
||||
//! - `archive_course(course_id)` - Archive a course (course owner)
|
||||
//!
|
||||
//! ### Storage
|
||||
//!
|
||||
//! - `Courses` - Course metadata indexed by course ID
|
||||
//! - `NextCourseId` - Auto-incrementing course ID counter
|
||||
//! - `Enrollments` - Student enrollment records (student, course_id) → Enrollment
|
||||
//! - `StudentCourses` - Per-student list of enrolled course IDs
|
||||
//!
|
||||
//! ### Integration
|
||||
//!
|
||||
//! - Implements `PerwerdeScoreProvider` trait for `pezpallet-trust`
|
||||
//! - Education scores contribute to validator eligibility
|
||||
//! - Course completion history visible to governance
|
||||
//!
|
||||
//! ## Security Features
|
||||
//!
|
||||
//! - Only course owners can mark completions
|
||||
//! - Active courses required for enrollment
|
||||
//! - No duplicate enrollments
|
||||
//! - Maximum courses per student limit
|
||||
//! - Admin-only course creation
|
||||
//!
|
||||
//! ## Runtime Integration Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! impl pezpallet_perwerde::Config for Runtime {
|
||||
//! type RuntimeEvent = RuntimeEvent;
|
||||
//! type AdminOrigin = EnsureRoot<AccountId>;
|
||||
//! type WeightInfo = pezpallet_perwerde::weights::BizinikiwiWeight<Runtime>;
|
||||
//! type MaxCourseNameLength = ConstU32<128>;
|
||||
//! type MaxCourseDescLength = ConstU32<512>;
|
||||
//! type MaxCourseLinkLength = ConstU32<256>;
|
||||
//! type MaxStudentsPerCourse = ConstU32<100>;
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
pub mod weights;
|
||||
|
||||
// These modules should only be compiled in `std` environment.
|
||||
#[cfg(all(feature = "std", any(test, feature = "runtime-benchmarks")))]
|
||||
pub mod mock;
|
||||
|
||||
#[cfg(all(feature = "std", test))]
|
||||
mod tests;
|
||||
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use pezframe_support::{
|
||||
dispatch::DispatchResult,
|
||||
pezpallet_prelude::*,
|
||||
traits::{EnsureOrigin, Get},
|
||||
};
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config {
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
#[pallet::constant]
|
||||
type MaxCourseNameLength: Get<u32>;
|
||||
#[pallet::constant]
|
||||
type MaxCourseDescLength: Get<u32>;
|
||||
#[pallet::constant]
|
||||
type MaxCourseLinkLength: Get<u32>;
|
||||
#[pallet::constant]
|
||||
type MaxStudentsPerCourse: Get<u32>;
|
||||
|
||||
/// Maximum number of courses a single student can enroll in
|
||||
/// Used for StudentCourses storage bound
|
||||
#[pallet::constant]
|
||||
type MaxCoursesPerStudent: Get<u32>;
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
pub enum CourseStatus {
|
||||
Active,
|
||||
Archived,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct Course<T: Config> {
|
||||
pub id: u32,
|
||||
pub owner: T::AccountId,
|
||||
pub name: BoundedVec<u8, T::MaxCourseNameLength>,
|
||||
pub description: BoundedVec<u8, T::MaxCourseDescLength>,
|
||||
pub content_link: BoundedVec<u8, T::MaxCourseLinkLength>,
|
||||
pub status: CourseStatus,
|
||||
pub created_at: BlockNumberFor<T>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct Enrollment<T: Config> {
|
||||
pub student: T::AccountId,
|
||||
pub course_id: u32,
|
||||
pub enrolled_at: BlockNumberFor<T>,
|
||||
pub completed_at: Option<BlockNumberFor<T>>,
|
||||
pub points_earned: u32,
|
||||
}
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn courses)]
|
||||
pub type Courses<T: Config> = StorageMap<_, Blake2_128Concat, u32, Course<T>, OptionQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn next_course_id)]
|
||||
pub type NextCourseId<T: Config> = StorageValue<_, u32, ValueQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn enrollments)]
|
||||
pub type Enrollments<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, (T::AccountId, u32), Enrollment<T>, OptionQuery>;
|
||||
|
||||
/// Per-student list of enrolled course IDs
|
||||
/// UPDATED (Gemini suggestion): Uses MaxCoursesPerStudent instead of MaxStudentsPerCourse
|
||||
/// This is the correct semantic - limits how many courses ONE student can take
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn student_courses)]
|
||||
pub type StudentCourses<T: Config> = StorageMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
T::AccountId,
|
||||
BoundedVec<u32, T::MaxCoursesPerStudent>,
|
||||
ValueQuery,
|
||||
>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
CourseCreated { course_id: u32, owner: T::AccountId },
|
||||
StudentEnrolled { student: T::AccountId, course_id: u32 },
|
||||
CourseCompleted { student: T::AccountId, course_id: u32, points: u32 },
|
||||
CourseArchived { course_id: u32 },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
CourseNotFound,
|
||||
AlreadyEnrolled,
|
||||
NotEnrolled,
|
||||
CourseNotActive,
|
||||
CourseAlreadyCompleted,
|
||||
NotCourseOwner,
|
||||
TooManyCourses,
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(T::WeightInfo::create_course())]
|
||||
pub fn create_course(
|
||||
origin: OriginFor<T>,
|
||||
name: BoundedVec<u8, T::MaxCourseNameLength>,
|
||||
description: BoundedVec<u8, T::MaxCourseDescLength>,
|
||||
content_link: BoundedVec<u8, T::MaxCourseLinkLength>,
|
||||
) -> DispatchResult {
|
||||
let owner = T::AdminOrigin::ensure_origin(origin)?;
|
||||
let course_id = NextCourseId::<T>::get();
|
||||
|
||||
// Parameters are already bounded, no conversion needed
|
||||
let course = Course {
|
||||
id: course_id,
|
||||
owner: owner.clone(),
|
||||
name,
|
||||
description,
|
||||
content_link,
|
||||
status: CourseStatus::Active,
|
||||
created_at: pezframe_system::Pallet::<T>::block_number(),
|
||||
};
|
||||
|
||||
Courses::<T>::insert(course_id, course);
|
||||
NextCourseId::<T>::mutate(|id| *id += 1);
|
||||
|
||||
Self::deposit_event(Event::CourseCreated { course_id, owner });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(T::WeightInfo::enroll())]
|
||||
pub fn enroll(origin: OriginFor<T>, course_id: u32) -> DispatchResult {
|
||||
let student = ensure_signed(origin)?;
|
||||
let course = Courses::<T>::get(course_id).ok_or(Error::<T>::CourseNotFound)?;
|
||||
ensure!(course.status == CourseStatus::Active, Error::<T>::CourseNotActive);
|
||||
ensure!(
|
||||
!Enrollments::<T>::contains_key((&student, course_id)),
|
||||
Error::<T>::AlreadyEnrolled
|
||||
);
|
||||
|
||||
let enrollment = Enrollment {
|
||||
student: student.clone(),
|
||||
course_id,
|
||||
enrolled_at: pezframe_system::Pallet::<T>::block_number(),
|
||||
completed_at: None,
|
||||
points_earned: 0,
|
||||
};
|
||||
|
||||
Enrollments::<T>::insert((&student, course_id), enrollment);
|
||||
StudentCourses::<T>::try_mutate(&student, |courses| {
|
||||
courses.try_push(course_id).map_err(|_| Error::<T>::TooManyCourses)
|
||||
})?;
|
||||
|
||||
Self::deposit_event(Event::StudentEnrolled { student, course_id });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark a student's course as completed and award points
|
||||
/// SECURITY: Only the course owner can mark completions, not students themselves
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(T::WeightInfo::complete_course())]
|
||||
pub fn complete_course(
|
||||
origin: OriginFor<T>,
|
||||
student: T::AccountId,
|
||||
course_id: u32,
|
||||
points: u32,
|
||||
) -> DispatchResult {
|
||||
let caller = ensure_signed(origin)?;
|
||||
|
||||
// Verify caller is the course owner
|
||||
let course = Courses::<T>::get(course_id).ok_or(Error::<T>::CourseNotFound)?;
|
||||
ensure!(course.owner == caller, Error::<T>::NotCourseOwner);
|
||||
|
||||
// Get and validate enrollment
|
||||
let mut enrollment =
|
||||
Enrollments::<T>::get((&student, course_id)).ok_or(Error::<T>::NotEnrolled)?;
|
||||
ensure!(enrollment.completed_at.is_none(), Error::<T>::CourseAlreadyCompleted);
|
||||
|
||||
// Mark completion
|
||||
enrollment.completed_at = Some(pezframe_system::Pallet::<T>::block_number());
|
||||
enrollment.points_earned = points;
|
||||
|
||||
Enrollments::<T>::insert((&student, course_id), enrollment);
|
||||
|
||||
Self::deposit_event(Event::CourseCompleted { student, course_id, points });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(3)]
|
||||
#[pallet::weight(T::WeightInfo::archive_course())]
|
||||
pub fn archive_course(origin: OriginFor<T>, course_id: u32) -> DispatchResult {
|
||||
let caller = T::AdminOrigin::ensure_origin(origin)?;
|
||||
let mut course = Courses::<T>::get(course_id).ok_or(Error::<T>::CourseNotFound)?;
|
||||
ensure!(course.owner == caller, Error::<T>::NotCourseOwner);
|
||||
|
||||
course.status = CourseStatus::Archived;
|
||||
Courses::<T>::insert(course_id, course);
|
||||
|
||||
Self::deposit_event(Event::CourseArchived { course_id });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
pub fn get_perwerde_score(who: &T::AccountId) -> u32 {
|
||||
StudentCourses::<T>::get(who)
|
||||
.iter()
|
||||
.filter_map(|course_id| Enrollments::<T>::get((who, *course_id)))
|
||||
.filter(|enrollment| enrollment.completed_at.is_some())
|
||||
.map(|enrollment| enrollment.points_earned)
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
use crate as pezpallet_perwerde;
|
||||
use pezframe_support::{
|
||||
construct_runtime, parameter_types,
|
||||
traits::{ConstU128, ConstU16, ConstU32, ConstU64, Everything, SortedMembers},
|
||||
};
|
||||
use pezframe_system::EnsureRoot;
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::{
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
BuildStorage,
|
||||
};
|
||||
|
||||
// Temel tipleri tanımlıyoruz.
|
||||
pub type AccountId = u64;
|
||||
pub type Balance = u128;
|
||||
pub type BlockNumber = u64;
|
||||
pub type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
|
||||
// Test runtime'ımızı kuruyoruz.
|
||||
construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
Perwerde: pezpallet_perwerde,
|
||||
Council: pezpallet_collective::<Instance1>,
|
||||
}
|
||||
);
|
||||
|
||||
// pezframe_system için implementasyon.
|
||||
impl pezframe_system::Config for Test {
|
||||
type BaseCallFilter = Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Nonce = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Block = Block;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = ConstU64<250>;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pezpallet_balances::AccountData<Balance>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ConstU16<42>;
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = ConstU32<16>;
|
||||
type RuntimeTask = ();
|
||||
type ExtensionsWeightInfo = ();
|
||||
type SingleBlockMigrations = ();
|
||||
type MultiBlockMigrator = ();
|
||||
type PreInherents = ();
|
||||
type PostInherents = ();
|
||||
type PostTransactions = ();
|
||||
}
|
||||
|
||||
// pezpallet_balances için implementasyon.
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type Balance = Balance;
|
||||
type DustRemoval = ();
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ExistentialDeposit = ConstU128<1>;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type MaxLocks = ConstU32<50>;
|
||||
type MaxReserves = ();
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ConstU32<1>;
|
||||
type RuntimeHoldReason = ();
|
||||
type RuntimeFreezeReason = ();
|
||||
type DoneSlashHandler = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const MaxCourseNameLength: u32 = 100;
|
||||
pub const MaxCourseDescLength: u32 = 500;
|
||||
pub const MaxCourseLinkLength: u32 = 200;
|
||||
pub const MaxStudentsPerCourse: u32 = 100; // Reduced for test performance
|
||||
pub const MaxCoursesPerStudent: u32 = 50; // Max courses a student can enroll in
|
||||
}
|
||||
|
||||
// --- KESİN ÇÖZÜM BURADA BAŞLIYOR ---
|
||||
|
||||
// AdminOrigin'i test etmek için kendi özel yetki sağlayıcımızı oluşturuyoruz.
|
||||
use pezframe_system::EnsureSignedBy;
|
||||
|
||||
// Bu struct, derleyicinin talep ettiği `SortedMembers` trait'ini manuel olarak uygular.
|
||||
// Bu, harici ve versiyona bağımlı araçlara olan ihtiyacı ortadan kaldırır.
|
||||
pub struct TestAdminProvider;
|
||||
impl SortedMembers<AccountId> for TestAdminProvider {
|
||||
fn sorted_members() -> Vec<AccountId> {
|
||||
// Test için admin olarak sadece 0 ID'li hesabı yetkili kılıyoruz.
|
||||
vec![0]
|
||||
}
|
||||
}
|
||||
|
||||
impl pezpallet_perwerde::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
// AdminOrigin'i, kendi yazdığımız ve sadece 0'ı admin kabul eden sağlayıcıya bağlıyoruz.
|
||||
type AdminOrigin = EnsureSignedBy<TestAdminProvider, AccountId>;
|
||||
type WeightInfo = ();
|
||||
type MaxCourseNameLength = MaxCourseNameLength;
|
||||
type MaxCourseDescLength = MaxCourseDescLength;
|
||||
type MaxCourseLinkLength = MaxCourseLinkLength;
|
||||
type MaxStudentsPerCourse = MaxStudentsPerCourse;
|
||||
type MaxCoursesPerStudent = MaxCoursesPerStudent;
|
||||
}
|
||||
|
||||
// Council Paletinin Mock Kurulumu (construct_runtime'da gerekli olduğu için kalıyor)
|
||||
use pezpallet_collective::Instance1;
|
||||
parameter_types! {
|
||||
pub const CouncilMotionDuration: BlockNumber = 5 * 60; // 5 minutes
|
||||
pub const CouncilMaxProposals: u32 = 100;
|
||||
pub const CouncilMaxMembers: u32 = 100;
|
||||
pub MaxProposalWeight: pezframe_support::weights::Weight = pezframe_support::weights::Weight::from_parts(1_000_000_000, 0);
|
||||
}
|
||||
impl pezpallet_collective::Config<Instance1> for Test {
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type Proposal = RuntimeCall;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type MotionDuration = CouncilMotionDuration;
|
||||
type MaxProposals = CouncilMaxProposals;
|
||||
type MaxMembers = CouncilMaxMembers;
|
||||
type DefaultVote = pezpallet_collective::PrimeDefaultVote;
|
||||
type WeightInfo = ();
|
||||
type SetMembersOrigin = EnsureRoot<AccountId>;
|
||||
type MaxProposalWeight = MaxProposalWeight;
|
||||
type DisapproveOrigin = EnsureRoot<AccountId>;
|
||||
type KillOrigin = EnsureRoot<AccountId>;
|
||||
type Consideration = ();
|
||||
}
|
||||
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
// `pezpallet-collective`'in genesis'ini de kurmamıza gerek kalmadı çünkü artık testimiz ona bağlı
|
||||
// değil.
|
||||
let mut ext = pezsp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
use crate::{
|
||||
mock::{new_test_ext, Perwerde as PerwerdePallet, RuntimeOrigin, System, Test},
|
||||
Event,
|
||||
};
|
||||
use pezframe_support::{assert_noop, assert_ok, pezpallet_prelude::Get, BoundedVec};
|
||||
use pezsp_runtime::DispatchError;
|
||||
|
||||
fn create_bounded_vec<L: Get<u32>>(s: &[u8]) -> BoundedVec<u8, L> {
|
||||
s.to_vec().try_into().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_course_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Admin olarak mock.rs'te TestAdminProvider içinde tanımladığımız hesabı kullanıyoruz.
|
||||
let admin_account_id = 0;
|
||||
|
||||
// Eylem: Yetkili admin ile kurs oluştur.
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin_account_id),
|
||||
create_bounded_vec(b"Blockchain 101"),
|
||||
create_bounded_vec(b"Giris seviyesi"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Doğrulama
|
||||
assert!(crate::Courses::<Test>::contains_key(0));
|
||||
let course = crate::Courses::<Test>::get(0).unwrap();
|
||||
assert_eq!(course.owner, admin_account_id);
|
||||
System::assert_last_event(
|
||||
Event::CourseCreated { course_id: 0, owner: admin_account_id }.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_course_fails_for_non_admin() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Admin (0) dışındaki bir hesap (2) kurs oluşturamaz.
|
||||
let non_admin = 2;
|
||||
assert_noop!(
|
||||
PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(non_admin),
|
||||
create_bounded_vec(b"Hacking 101"),
|
||||
create_bounded_vec(b"Yetkisiz kurs"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
),
|
||||
DispatchError::BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ENROLL TESTS (8 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn enroll_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create course first
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Rust Basics"),
|
||||
create_bounded_vec(b"Learn Rust"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Student enrolls
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// Verify enrollment
|
||||
let enrollment = crate::Enrollments::<Test>::get((student, 0)).unwrap();
|
||||
assert_eq!(enrollment.student, student);
|
||||
assert_eq!(enrollment.course_id, 0);
|
||||
assert_eq!(enrollment.completed_at, None);
|
||||
assert_eq!(enrollment.points_earned, 0);
|
||||
|
||||
// Verify StudentCourses updated
|
||||
let student_courses = crate::StudentCourses::<Test>::get(student);
|
||||
assert!(student_courses.contains(&0));
|
||||
|
||||
System::assert_last_event(Event::StudentEnrolled { student, course_id: 0 }.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enroll_fails_for_nonexistent_course() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let student = 1;
|
||||
assert_noop!(
|
||||
PerwerdePallet::enroll(RuntimeOrigin::signed(student), 999),
|
||||
crate::Error::<Test>::CourseNotFound
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enroll_fails_for_archived_course() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create and archive course
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Old Course"),
|
||||
create_bounded_vec(b"Archived"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::archive_course(RuntimeOrigin::signed(admin), 0));
|
||||
|
||||
// Try to enroll in archived course
|
||||
assert_noop!(
|
||||
PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0),
|
||||
crate::Error::<Test>::CourseNotActive
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enroll_fails_if_already_enrolled() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create course
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Description"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// First enrollment succeeds
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// Second enrollment fails
|
||||
assert_noop!(
|
||||
PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0),
|
||||
crate::Error::<Test>::AlreadyEnrolled
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_students_can_enroll_same_course() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student1 = 1;
|
||||
let student2 = 2;
|
||||
let student3 = 3;
|
||||
|
||||
// Create course
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Popular Course"),
|
||||
create_bounded_vec(b"Many students"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Multiple students enroll
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student1), 0));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student2), 0));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student3), 0));
|
||||
|
||||
// Verify all enrollments
|
||||
assert!(crate::Enrollments::<Test>::contains_key((student1, 0)));
|
||||
assert!(crate::Enrollments::<Test>::contains_key((student2, 0)));
|
||||
assert!(crate::Enrollments::<Test>::contains_key((student3, 0)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn student_can_enroll_multiple_courses() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create 3 courses
|
||||
for i in 0..3 {
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(format!("Course {}", i).as_bytes()),
|
||||
create_bounded_vec(b"Description"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
}
|
||||
|
||||
// Student enrolls in all 3
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 1));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 2));
|
||||
|
||||
// Verify StudentCourses
|
||||
let student_courses = crate::StudentCourses::<Test>::get(student);
|
||||
assert_eq!(student_courses.len(), 3);
|
||||
assert!(student_courses.contains(&0));
|
||||
assert!(student_courses.contains(&1));
|
||||
assert!(student_courses.contains(&2));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enroll_fails_when_too_many_courses() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// MaxCoursesPerStudent is 50, so create and enroll in 50 courses
|
||||
for i in 0..50 {
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(format!("Course {}", i).as_bytes()),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), i));
|
||||
}
|
||||
|
||||
// Create one more course
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course 50"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Enrollment should fail - student already enrolled in max courses
|
||||
assert_noop!(
|
||||
PerwerdePallet::enroll(RuntimeOrigin::signed(student), 50),
|
||||
crate::Error::<Test>::TooManyCourses
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enroll_event_emitted_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 5;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Test"),
|
||||
create_bounded_vec(b"Test"),
|
||||
create_bounded_vec(b"http://test.com")
|
||||
));
|
||||
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
System::assert_last_event(Event::StudentEnrolled { student: 5, course_id: 0 }.into());
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// COMPLETE_COURSE TESTS (8 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn complete_course_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
let points = 95;
|
||||
|
||||
// Setup: Create course and enroll
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// Complete the course (course owner completes for student)
|
||||
assert_ok!(PerwerdePallet::complete_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
student,
|
||||
0,
|
||||
points
|
||||
));
|
||||
|
||||
// Verify completion
|
||||
let enrollment = crate::Enrollments::<Test>::get((student, 0)).unwrap();
|
||||
assert!(enrollment.completed_at.is_some());
|
||||
assert_eq!(enrollment.points_earned, points);
|
||||
|
||||
System::assert_last_event(Event::CourseCompleted { student, course_id: 0, points }.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_course_fails_without_enrollment() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create course but don't enroll
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Try to complete without enrollment (admin tries to complete for non-enrolled student)
|
||||
assert_noop!(
|
||||
PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 0, 100),
|
||||
crate::Error::<Test>::NotEnrolled
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_course_fails_if_already_completed() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Setup
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// First completion succeeds (admin completes)
|
||||
assert_ok!(PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 0, 85));
|
||||
|
||||
// Second completion fails
|
||||
assert_noop!(
|
||||
PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 0, 90),
|
||||
crate::Error::<Test>::CourseAlreadyCompleted
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_course_with_zero_points() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// Complete with 0 points (failed course)
|
||||
assert_ok!(PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 0, 0));
|
||||
|
||||
let enrollment = crate::Enrollments::<Test>::get((student, 0)).unwrap();
|
||||
assert_eq!(enrollment.points_earned, 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_course_with_max_points() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// Complete with maximum points
|
||||
assert_ok!(PerwerdePallet::complete_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
student,
|
||||
0,
|
||||
u32::MAX
|
||||
));
|
||||
|
||||
let enrollment = crate::Enrollments::<Test>::get((student, 0)).unwrap();
|
||||
assert_eq!(enrollment.points_earned, u32::MAX);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_students_complete_same_course() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// 3 students enroll and admin completes with different scores
|
||||
for i in 1u64..=3 {
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(i), 0));
|
||||
assert_ok!(PerwerdePallet::complete_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
i,
|
||||
0,
|
||||
(70 + (i * 10)) as u32
|
||||
));
|
||||
}
|
||||
|
||||
// Verify each completion
|
||||
for i in 1u64..=3 {
|
||||
let enrollment = crate::Enrollments::<Test>::get((i, 0)).unwrap();
|
||||
assert!(enrollment.completed_at.is_some());
|
||||
assert_eq!(enrollment.points_earned, (70 + (i * 10)) as u32);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn student_completes_multiple_courses() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create 3 courses
|
||||
for i in 0..3 {
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(format!("Course {}", i).as_bytes()),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), i));
|
||||
}
|
||||
|
||||
// Complete all 3 (admin completes for student)
|
||||
assert_ok!(PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 0, 80));
|
||||
assert_ok!(PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 1, 90));
|
||||
assert_ok!(PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 2, 95));
|
||||
|
||||
// Verify all completions
|
||||
for i in 0..3 {
|
||||
let enrollment = crate::Enrollments::<Test>::get((student, i)).unwrap();
|
||||
assert!(enrollment.completed_at.is_some());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_course_event_emitted() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 7;
|
||||
let points = 88;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Test"),
|
||||
create_bounded_vec(b"Test"),
|
||||
create_bounded_vec(b"http://test.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
assert_ok!(PerwerdePallet::complete_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
student,
|
||||
0,
|
||||
points
|
||||
));
|
||||
|
||||
System::assert_last_event(
|
||||
Event::CourseCompleted { student: 7, course_id: 0, points: 88 }.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ARCHIVE_COURSE TESTS (4 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn archive_course_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
assert_ok!(PerwerdePallet::archive_course(RuntimeOrigin::signed(admin), 0));
|
||||
|
||||
let course = crate::Courses::<Test>::get(0).unwrap();
|
||||
assert_eq!(course.status, crate::CourseStatus::Archived);
|
||||
|
||||
System::assert_last_event(Event::CourseArchived { course_id: 0 }.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_course_fails_for_non_owner() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let other_user = 1;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Non-owner cannot archive
|
||||
assert_noop!(
|
||||
PerwerdePallet::archive_course(RuntimeOrigin::signed(other_user), 0),
|
||||
DispatchError::BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_course_fails_for_nonexistent_course() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
|
||||
assert_noop!(
|
||||
PerwerdePallet::archive_course(RuntimeOrigin::signed(admin), 999),
|
||||
crate::Error::<Test>::CourseNotFound
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archived_course_cannot_accept_new_enrollments() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create and archive
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::archive_course(RuntimeOrigin::signed(admin), 0));
|
||||
|
||||
// Try to enroll - should fail
|
||||
assert_noop!(
|
||||
PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0),
|
||||
crate::Error::<Test>::CourseNotActive
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INTEGRATION & STORAGE TESTS (2 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn storage_consistency_check() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create course
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Enroll
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// Check storage consistency
|
||||
assert!(crate::Courses::<Test>::contains_key(0));
|
||||
assert!(crate::Enrollments::<Test>::contains_key((student, 0)));
|
||||
|
||||
let student_courses = crate::StudentCourses::<Test>::get(student);
|
||||
assert_eq!(student_courses.len(), 1);
|
||||
assert!(student_courses.contains(&0));
|
||||
|
||||
let enrollment = crate::Enrollments::<Test>::get((student, 0)).unwrap();
|
||||
assert_eq!(enrollment.course_id, 0);
|
||||
assert_eq!(enrollment.student, student);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_course_id_increments_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
|
||||
assert_eq!(crate::NextCourseId::<Test>::get(), 0);
|
||||
|
||||
// Create 5 courses
|
||||
for i in 0..5 {
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(format!("Course {}", i).as_bytes()),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
assert_eq!(crate::NextCourseId::<Test>::get(), i + 1);
|
||||
}
|
||||
|
||||
// Verify all courses exist
|
||||
for i in 0..5 {
|
||||
assert!(crate::Courses::<Test>::contains_key(i));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
//! Autogenerated weights for `pezpallet_perwerde`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm
|
||||
// --pallets
|
||||
// pezpallet_perwerde
|
||||
// -e
|
||||
// all
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --output
|
||||
// pezcumulus/teyrchains/pezpallets/perwerde/src/weights.rs
|
||||
// --template
|
||||
// bizinikiwi/.maintain/frame-weight-template.hbs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_perwerde`.
|
||||
pub trait WeightInfo {
|
||||
fn create_course() -> Weight;
|
||||
fn enroll() -> Weight;
|
||||
fn complete_course() -> Weight;
|
||||
fn archive_course() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_perwerde` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `Perwerde::NextCourseId` (r:1 w:1)
|
||||
/// Proof: `Perwerde::NextCourseId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::Courses` (r:0 w:1)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
fn create_course() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3`
|
||||
// Estimated: `1489`
|
||||
// Minimum execution time: 21_032_000 picoseconds.
|
||||
Weight::from_parts(22_777_000, 1489)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Perwerde::Courses` (r:1 w:0)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::Enrollments` (r:1 w:1)
|
||||
/// Proof: `Perwerde::Enrollments` (`max_values`: None, `max_size`: Some(101), added: 2576, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::StudentCourses` (r:1 w:1)
|
||||
/// Proof: `Perwerde::StudentCourses` (`max_values`: None, `max_size`: Some(249), added: 2724, mode: `MaxEncodedLen`)
|
||||
fn enroll() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `149`
|
||||
// Estimated: `4428`
|
||||
// Minimum execution time: 33_652_000 picoseconds.
|
||||
Weight::from_parts(37_083_000, 4428)
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Perwerde::Courses` (r:1 w:0)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::Enrollments` (r:1 w:1)
|
||||
/// Proof: `Perwerde::Enrollments` (`max_values`: None, `max_size`: Some(101), added: 2576, mode: `MaxEncodedLen`)
|
||||
fn complete_course() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `307`
|
||||
// Estimated: `4428`
|
||||
// Minimum execution time: 33_123_000 picoseconds.
|
||||
Weight::from_parts(37_458_000, 4428)
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Perwerde::Courses` (r:1 w:1)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
fn archive_course() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `149`
|
||||
// Estimated: `4428`
|
||||
// Minimum execution time: 24_529_000 picoseconds.
|
||||
Weight::from_parts(27_529_000, 4428)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `Perwerde::NextCourseId` (r:1 w:1)
|
||||
/// Proof: `Perwerde::NextCourseId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::Courses` (r:0 w:1)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
fn create_course() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3`
|
||||
// Estimated: `1489`
|
||||
// Minimum execution time: 21_032_000 picoseconds.
|
||||
Weight::from_parts(22_777_000, 1489)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Perwerde::Courses` (r:1 w:0)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::Enrollments` (r:1 w:1)
|
||||
/// Proof: `Perwerde::Enrollments` (`max_values`: None, `max_size`: Some(101), added: 2576, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::StudentCourses` (r:1 w:1)
|
||||
/// Proof: `Perwerde::StudentCourses` (`max_values`: None, `max_size`: Some(249), added: 2724, mode: `MaxEncodedLen`)
|
||||
fn enroll() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `149`
|
||||
// Estimated: `4428`
|
||||
// Minimum execution time: 33_652_000 picoseconds.
|
||||
Weight::from_parts(37_083_000, 4428)
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Perwerde::Courses` (r:1 w:0)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::Enrollments` (r:1 w:1)
|
||||
/// Proof: `Perwerde::Enrollments` (`max_values`: None, `max_size`: Some(101), added: 2576, mode: `MaxEncodedLen`)
|
||||
fn complete_course() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `307`
|
||||
// Estimated: `4428`
|
||||
// Minimum execution time: 33_123_000 picoseconds.
|
||||
Weight::from_parts(37_458_000, 4428)
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Perwerde::Courses` (r:1 w:1)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
fn archive_course() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `149`
|
||||
// Estimated: `4428`
|
||||
// Minimum execution time: 24_529_000 picoseconds.
|
||||
Weight::from_parts(27_529_000, 4428)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user