From 890fe56a1632827d72358c8ab4bcb68ce5659f3e Mon Sep 17 00:00:00 2001 From: realaravinth Date: Sun, 28 Feb 2021 15:05:39 +0530 Subject: [PATCH] readme, CI and rewrote levels --- .github/workflows/linux.yml | 80 +++++++ .gitignore | 1 + Cargo.lock | 75 +++++++ Cargo.toml | 5 +- README.md | 28 +++ src/counter.rs | 158 ++++++++++++++ src/errors.rs | 34 +++ src/levels.rs | 162 ++++++++++++++ src/lib.rs | 4 + src/main.rs | 3 - src/new_levels.rs | 424 ++++++++++++++++++++++++++++++++++++ 11 files changed, 970 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/linux.yml create mode 100644 README.md create mode 100644 src/counter.rs create mode 100644 src/errors.rs create mode 100644 src/levels.rs create mode 100644 src/lib.rs delete mode 100644 src/main.rs create mode 100644 src/new_levels.rs diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 0000000..159c5cb --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,80 @@ +name: CI (Linux) + +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master + + +jobs: + build_and_test: + strategy: + fail-fast: false + matrix: + version: + - stable + - nightly + + name: ${{ matrix.version }} - x86_64-unknown-linux-gnu + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: ⚡ Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install ${{ matrix.version }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: check build + uses: actions-rs/cargo@v1 + with: + command: check + args: --all --bins --examples --tests + + - name: tests + uses: actions-rs/cargo@v1 + timeout-minutes: 40 + with: + command: test + args: --all --all-features --no-fail-fast + + - name: Generate coverage file + if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') + uses: actions-rs/tarpaulin@v0.1 + with: + version: '0.15.0' + args: '-t 1200' + + - name: Upload to Codecov + if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') + uses: codecov/codecov-action@v1 + with: + file: cobertura.xml + + - name: generate documentation + if: matrix.version == 'stable' && (github.repository == 'realaravinth/mCaptcha') + uses: actions-rs/cargo@v1 + with: + command: doc + args: --no-deps --workspace --all-features + + - name: Deploy to GitHub Pages + if: matrix.version == 'stable' && (github.repository == 'realaravinth/mCaptcha') + uses: JamesIves/github-pages-deploy-action@3.7.1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages + FOLDER: target/doc diff --git a/.gitignore b/.gitignore index ea8c4bf..d5aabfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +tarpaulin-report.html diff --git a/Cargo.lock b/Cargo.lock index c95f591..9f12b96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,66 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" +dependencies = [ + "darling", + "derive_builder_core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_more" version = "0.99.11" @@ -371,6 +431,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.2" @@ -487,7 +553,10 @@ name = "m_captcha" version = "0.1.0" dependencies = [ "actix", + "actix-rt", + "derive_builder", "derive_more", + "lazy_static", "serde", "serde_json", ] @@ -839,6 +908,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "syn" version = "1.0.60" diff --git a/Cargo.toml b/Cargo.toml index 1ece608..8b31dd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,9 @@ members = [ ".", "browser", "cli" ] [dependencies] actix = "0.10" -derive_more = "0.99" serde = "1.0.114" serde_json = "1" +derive_builder = "0.9" +derive_more = "0.99" +lazy_static = "1.4" +actix-rt = "1" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b50529e --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +
+

mCaptcha

+

+ mCaptcha - PoW based DoS protection +

+ +[![Documentation](https://img.shields.io/badge/docs-master-blue)](https://realaravinth.github.io/mCaptcha/m_captcha/index.html) +![CI (Linux)]() +[![dependency status](https://deps.rs/repo/github/realaravinth/mCaptcha/status.svg)](https://deps.rs/repo/github/realaravinth/mCaptcha) +
+[![codecov](https://codecov.io/gh/realaravinth/mCaptcha/branch/master/graph/badge.svg)](https://codecov.io/gh/realaravinth/mCaptcha) + +
+ +## Features +TODO + +## Usage: + +Add this to your `Cargo.toml`: + +```toml +mCaptcha = { version = "0.1", git = "https://github.com/realaravinth/mCaptcha" } +``` + +## Examples: + +TODO diff --git a/src/counter.rs b/src/counter.rs new file mode 100644 index 0000000..42bc025 --- /dev/null +++ b/src/counter.rs @@ -0,0 +1,158 @@ +use std::time::Duration; + +use actix::prelude::*; +use derive_builder::Builder; +//use lazy_static::*; + +use crate::levels::Levels; + +// TODO move this into config parameter +// lazy_static! { +// pub static ref DURATION: Duration = Duration::new(POW_SESSION_DURATION, 0); +// } + +/// Add visitor message +#[derive(Message)] +#[rtype(result = "u32")] +pub struct Visitor; + +#[derive(Message)] +#[rtype(result = "()")] +struct DeleteVisitor; + +#[derive(Builder)] +pub struct Counter { + visitor_count: usize, + levels: Levels, + duration: u64, +} + +impl Default for Counter { + fn default() -> Self { + Counter { + visitor_count: 0, + levels: Levels::default(), + duration: 30, + } + } +} + +impl Actor for Counter { + type Context = Context; + + // fn started(&mut self, ctx: &mut Self::Context) { + // ctx.set_mailbox_capacity(usize::MAX / 2); + // } +} + +impl Handler for Counter { + type Result = u32; + fn handle(&mut self, _: Visitor, ctx: &mut Self::Context) -> Self::Result { + use actix::clock::delay_for; + + self.visitor_count += 1; + + let addr = ctx.address(); + + let duration: Duration = Duration::new(self.duration.clone(), 0); + let wait_for = async move { + delay_for(duration).await; + addr.send(DeleteVisitor).await.unwrap(); + } + .into_actor(self); + ctx.spawn(wait_for); + + if self.visitor_count > self.levels.threshold() { + self.levels.focus(); + } else { + self.levels.relax(); + } + + self.levels.get_difficulty() + } +} + +impl Handler for Counter { + type Result = (); + fn handle(&mut self, _msg: DeleteVisitor, _ctx: &mut Self::Context) -> Self::Result { + if self.visitor_count > 0 { + self.visitor_count -= 1; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + async fn race(addr: Addr, count: Levels) { + for _ in 0..count as usize - 1 { + let _ = addr.send(Visitor).await.unwrap(); + } + } + #[actix_rt::test] + async fn counter_focus_works() { + let four = Levels::Four.get_difficulty(); + let three = Levels::Three.get_difficulty(); + let two = Levels::Two.get_difficulty(); + let one = Levels::One.get_difficulty(); + + let addr = Counter::default().start(); + + let mut difficulty_factor = addr.send(Visitor).await.unwrap(); + assert_eq!(difficulty_factor, one); + + let addr = Counter::default().start(); + race(addr.clone(), Levels::One).await; + difficulty_factor = addr.send(Visitor).await.unwrap(); + assert_eq!(difficulty_factor, one); + + let addr = Counter::default().start(); + race(addr.clone(), Levels::Two).await; + addr.send(Visitor).await.unwrap(); + difficulty_factor = addr.send(Visitor).await.unwrap(); + assert_eq!(difficulty_factor, two); + + let addr = Counter::default().start(); + race(addr.clone(), Levels::Three).await; + difficulty_factor = addr.send(Visitor).await.unwrap(); + assert_eq!(difficulty_factor, three); + + let addr = Counter::default().start(); + race(addr.clone(), Levels::Four).await; + addr.send(Visitor).await.unwrap(); + difficulty_factor = addr.send(Visitor).await.unwrap(); + assert_eq!(difficulty_factor, four); + } + + #[actix_rt::test] + async fn counter_relax_works() { + use actix::clock::delay_for; + let four = Levels::Four.get_difficulty(); + let three = Levels::Three.get_difficulty(); + let two = Levels::Two.get_difficulty(); + let one = Levels::One.get_difficulty(); + + let addr = Counter::default().start(); + + let mut difficulty_factor = addr.send(Visitor).await.unwrap(); + + let addr = Counter::default().start(); + race(addr.clone(), Levels::Four).await; + addr.send(Visitor).await.unwrap(); + difficulty_factor = addr.send(Visitor).await.unwrap(); + assert_eq!(difficulty_factor, four); + + // could break when default duration for counter actor changes + let duration = Duration::new(30, 0); + + delay_for(duration).await; + + difficulty_factor = addr.send(Visitor).await.unwrap(); + assert_eq!(difficulty_factor, three); + difficulty_factor = addr.send(Visitor).await.unwrap(); + assert_eq!(difficulty_factor, two); + difficulty_factor = addr.send(Visitor).await.unwrap(); + assert_eq!(difficulty_factor, one); + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..b666fe7 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,34 @@ +//! Error datatypes +use derive_more::{Display, Error}; + +/// Errors that can occur when interacting with the blockchain +#[derive(Debug, PartialEq, Display, Clone, Error)] +#[cfg(not(tarpaulin_include))] +pub enum CaptchaError { + /// when configuring m_captcha, `DefenseBuilder` must be passed atleast + /// one `LevelConfig` if not this error will arise + #[display(fmt = "LevelBuilder should have atleaset one level configured")] + LevelEmpty, + /// Visitor count must be an integer + /// when configuring m_captcha, `LevelBuilder` difficulty_factor + /// must be set to greater than zero. + #[display(fmt = "difficulty factor must be greater than zero")] + DifficultyFactorZero, + /// Difficulty factor must be set + #[display(fmt = "Set difficulty factor")] + SetDifficultyFactor, + /// Visitor count must be set + #[display(fmt = "Set visitor count")] + SetVisitorCount, + + /// Visitor count must be Unique + #[display(fmt = "Duplicate visitor count")] + DuplicateVisitorCount, + + /// Difficulty factor should increase with level + #[display(fmt = "Difficulty factor should increase with level")] + DecreaseingDifficultyFactor, +} + +/// [Result] datatype for m_captcha +pub type CaptchaResult = std::result::Result; diff --git a/src/levels.rs b/src/levels.rs new file mode 100644 index 0000000..64b5a55 --- /dev/null +++ b/src/levels.rs @@ -0,0 +1,162 @@ +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Levels { + One = 500, + Two = 1000, + Three = 2000, + Four = 4000, +} + +impl Default for Levels { + fn default() -> Self { + Levels::One + } +} + +impl Levels { + pub fn relax(&mut self) -> u32 { + self.rev().next(); + *self as u32 + } + + ///! Difficulty is calculated as + ///! ```rust + ///! let difficulty = u128::max_value() - u128::max_value() / difficulty_factor; + ///! ``` + ///! the higher the `difficulty_factor`, the higher the difficulty. + + pub fn get_difficulty(&self) -> u32 { + match self { + Levels::Three => 100_000, + Levels::Four => 1_000_000, + _ => *self as u32, + } + } + + pub fn threshold(&self) -> usize { + *self as usize + } + + pub fn focus(&mut self) -> u32 { + self.next(); + *self as u32 + } +} + +impl DoubleEndedIterator for Levels { + fn next_back(&mut self) -> Option { + match self { + Levels::One => { + *self = Levels::One; + } + Levels::Two => { + *self = Levels::One; + } + Levels::Three => { + *self = Levels::Two; + } + Levels::Four => { + *self = Levels::Three; + } + }; + + Some(()) + } +} + +impl Iterator for Levels { + type Item = (); + + fn next(&mut self) -> Option<()> { + match self { + Levels::One => { + *self = Levels::Two; + } + Levels::Two => { + *self = Levels::Three; + } + Levels::Three => { + *self = Levels::Four; + } + Levels::Four => { + *self = Levels::Four; + } + }; + + Some(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn levels_enum_works() { + let mut level = Levels::default(); + + assert_eq!(level, Levels::One); + + level.next(); + assert_eq!(level, Levels::Two); + + level.next(); + assert_eq!(level, Levels::Three); + + level.next(); + assert_eq!(level, Levels::Four); + + level.next(); + assert_eq!(level, Levels::Four); + } + + #[test] + fn focus_works() { + let mut level = Levels::default(); + + assert_eq!(level, Levels::One); + assert_eq!(level.focus(), Levels::Two as u32); + assert_eq!(level.focus(), Levels::Three as u32); + assert_eq!(level.focus(), Levels::Four as u32); + assert_eq!(level.focus(), Levels::Four as u32); + } + + #[test] + fn relax_works() { + let mut level = Levels::default(); + + assert_eq!(level, Levels::One); + assert_eq!(level.focus(), Levels::Two as u32); + assert_eq!(level.focus(), Levels::Three as u32); + assert_eq!(level.focus(), Levels::Four as u32); + + assert_eq!(level.relax(), Levels::Three as u32); + assert_eq!(level.relax(), Levels::Two as u32); + + assert_eq!(level.relax(), Levels::One as u32); + assert_eq!(level.relax(), Levels::One as u32); + } + + #[test] + fn threshold_works() { + let mut level = Levels::default(); + + assert_eq!(level.threshold(), Levels::One as usize); + level.next(); + assert_eq!(level.threshold(), Levels::Two as usize); + level.next(); + assert_eq!(level.threshold(), Levels::Three as usize); + } + + #[test] + fn difficulty_works() { + let mut level = Levels::default(); + + assert_eq!(level.get_difficulty(), Levels::One as u32); + level.next(); + assert_eq!(level.get_difficulty(), Levels::Two as u32); + level.next(); + assert_eq!(level.get_difficulty(), 100_000); + level.next(); + assert_eq!(level.get_difficulty(), 1_000_000); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7951a15 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod counter; +pub mod errors; +mod levels; +mod new_levels; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index e7a11a9..0000000 --- a/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} diff --git a/src/new_levels.rs b/src/new_levels.rs new file mode 100644 index 0000000..cf57e8e --- /dev/null +++ b/src/new_levels.rs @@ -0,0 +1,424 @@ +// //! ```rust +// //! DefenseBuilder::default() +// //! .add_level( +// //! LevelBuilder::default() +// //! .visitor_count(50) +// //! .difficulty_factor(50) +// //! .unwrap() +// //! .build() +// //! .unwrap(), +// //! ) +// //! .unwrap() +// //! .add_level( +// //! LevelBuilder::default() +// //! .visitor_count(500) +// //! .difficulty_factor(500) +// //! .unwrap() +// //! .build() +// //! .unwrap(), +// //! ) +// //! .unwrap() +// //! .build() +// //! .unwrap(); +// //! ``` + +use crate::errors::*; + +/// Level struct +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Level { + visitor_count: u32, + difficulty_factor: u32, +} + +impl Default for Level { + fn default() -> Self { + Level { + visitor_count: 0, + difficulty_factor: 0, + } + } +} + +/// set difficulty configuration +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct LevelBuilder { + visitor_count: Option, + difficulty_factor: Option, +} + +impl Default for LevelBuilder { + fn default() -> Self { + LevelBuilder { + visitor_count: None, + difficulty_factor: None, + } + } +} + +impl LevelBuilder { + /// set visitor count for level + pub fn visitor_count(&mut self, visitor_count: u32) -> &mut Self { + self.visitor_count = Some(visitor_count); + self + } + + /// set difficulty factor for level + /// difficulty_factor can't be zero because + /// Difficulty is calculated as + /// ```no_run + /// let difficulty_factor = 500; + /// let difficulty = u128::max_value() - u128::max_value() / difficulty_factor; + /// ``` + /// the higher the `difficulty_factor`, the higher the difficulty. + pub fn difficulty_factor(&mut self, difficulty_factor: u32) -> CaptchaResult<&mut Self> { + if difficulty_factor > 0 { + self.difficulty_factor = Some(difficulty_factor); + Ok(self) + } else { + Err(CaptchaError::DifficultyFactorZero) + } + } + + /// build Level + pub fn build(&mut self) -> CaptchaResult { + if self.visitor_count.is_none() { + Err(CaptchaError::SetVisitorCount) + } else if self.difficulty_factor.is_none() { + Err(CaptchaError::SetDifficultyFactor) + } else { + Ok(Level { + difficulty_factor: self.difficulty_factor.unwrap(), + visitor_count: self.visitor_count.unwrap(), + }) + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Defense { + levels: Vec, + // index of current visitor threshold + current_visitor_threshold: usize, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct DefenseBuilder { + levels: Vec, +} + +impl Default for DefenseBuilder { + fn default() -> Self { + DefenseBuilder { levels: vec![] } + } +} + +impl DefenseBuilder { + pub fn add_level(&mut self, level: Level) -> CaptchaResult<&mut Self> { + for i in self.levels.iter() { + if i.visitor_count == level.visitor_count { + return Err(CaptchaError::DuplicateVisitorCount); + } + } + self.levels.push(level); + Ok(self) + } + + pub fn build(&mut self) -> CaptchaResult { + if !self.levels.is_empty() { + // sort levels to arrange in ascending order + self.levels.sort_by_key(|a| a.visitor_count); + + // as visitor count increases, difficulty_factor too should increse + + for level in self.levels.iter() { + if level.difficulty_factor == 0 { + return Err(CaptchaError::DifficultyFactorZero); + } + } + + for i in 0..self.levels.len() - 1 { + if self.levels[i].difficulty_factor > self.levels[i + 1].difficulty_factor { + return Err(CaptchaError::DecreaseingDifficultyFactor); + } + } + + Ok(Defense { + levels: self.levels.to_owned(), + current_visitor_threshold: 0, + }) + } else { + Err(CaptchaError::LevelEmpty) + } + } +} + +impl Default for Defense { + fn default() -> Self { + Defense { + levels: vec![Level::default()], + current_visitor_threshold: 0, + } + } +} + +impl Defense { + ///! Difficulty is calculated as + ///! ```rust + ///! let difficulty = u128::max_value() - u128::max_value() / difficulty_factor; + ///! ``` + ///! the higher the `difficulty_factor`, the higher the difficulty. + + /// get difficulty factor of current level of defense + pub fn get_difficulty(&self) -> u32 { + self.levels[self.current_visitor_threshold].difficulty_factor + } + + /// tighten up defense. Increases defense level by a factor of one + /// when defense is at max level, calling this method will have no effect + pub fn tighten_up(&mut self) { + println!("{}", self.current_visitor_threshold); + if self.current_visitor_threshold != self.levels.len() - 1 { + self.current_visitor_threshold += 1; + println!("{}", self.current_visitor_threshold); + } + } + /// loosen up defense. Decreases defense level by a factor of one + /// when defense is at the lowest level, calling this method will have no effect + pub fn loosen_up(&mut self) { + if self.current_visitor_threshold != 0 { + self.current_visitor_threshold -= 1; + } + } + + /// set defense to maximum level + pub fn max_defense(&mut self) { + self.current_visitor_threshold = self.levels.len() - 1; + } + + /// set defense to minimum level + pub fn min_defense(&mut self) { + self.current_visitor_threshold = 0; + } + + pub fn visitor_threshold(&self) -> u32 { + self.levels[self.current_visitor_threshold].visitor_count + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn level_builder_works() { + let level = LevelBuilder::default() + .difficulty_factor(1) + .unwrap() + .visitor_count(0) + .build() + .unwrap(); + + assert_eq!(level.visitor_count, 0); + assert_eq!(level.difficulty_factor, 1); + + assert_eq!( + LevelBuilder::default().difficulty_factor(0), + Err(CaptchaError::DifficultyFactorZero) + ); + } + + #[test] + fn defense_builder_duplicate_visitor_count() { + let mut defense_builder = DefenseBuilder::default(); + let err = defense_builder + .add_level( + LevelBuilder::default() + .visitor_count(50) + .difficulty_factor(50) + .unwrap() + .build() + .unwrap(), + ) + .unwrap() + .add_level( + LevelBuilder::default() + .visitor_count(50) + .difficulty_factor(50) + .unwrap() + .build() + .unwrap(), + ); + assert_eq!(err, Err(CaptchaError::DuplicateVisitorCount)); + } + + #[test] + fn defense_builder_decreasing_difficulty_factor() { + let mut defense_builder = DefenseBuilder::default(); + let err = defense_builder + .add_level( + LevelBuilder::default() + .visitor_count(50) + .difficulty_factor(50) + .unwrap() + .build() + .unwrap(), + ) + .unwrap() + .add_level( + LevelBuilder::default() + .visitor_count(500) + .difficulty_factor(10) + .unwrap() + .build() + .unwrap(), + ) + .unwrap() + .build(); + assert_eq!(err, Err(CaptchaError::DecreaseingDifficultyFactor)); + } + + fn get_defense() -> Defense { + DefenseBuilder::default() + .add_level( + LevelBuilder::default() + .visitor_count(50) + .difficulty_factor(50) + .unwrap() + .build() + .unwrap(), + ) + .unwrap() + .add_level( + LevelBuilder::default() + .visitor_count(500) + .difficulty_factor(5000) + .unwrap() + .build() + .unwrap(), + ) + .unwrap() + .add_level( + LevelBuilder::default() + .visitor_count(5000) + .difficulty_factor(50000) + .unwrap() + .build() + .unwrap(), + ) + .unwrap() + .add_level( + LevelBuilder::default() + .visitor_count(50000) + .difficulty_factor(500000) + .unwrap() + .build() + .unwrap(), + ) + .unwrap() + .add_level( + LevelBuilder::default() + .visitor_count(500000) + .difficulty_factor(5000000) + .unwrap() + .build() + .unwrap(), + ) + .unwrap() + .build() + .unwrap() + } + #[test] + fn defense_builder_works() { + let defense = get_defense(); + + assert_eq!(defense.levels[0].difficulty_factor, 50); + assert_eq!(defense.levels[1].difficulty_factor, 5000); + assert_eq!(defense.levels[2].difficulty_factor, 50_000); + assert_eq!(defense.levels[3].difficulty_factor, 500_000); + assert_eq!(defense.levels[4].difficulty_factor, 5_000_000); + } + + #[test] + fn tighten_up_works() { + let mut defense = get_defense(); + + assert_eq!(defense.get_difficulty(), 50); + + defense.tighten_up(); + assert_eq!(defense.get_difficulty(), 5_000); + + defense.tighten_up(); + assert_eq!(defense.get_difficulty(), 50_000); + + defense.tighten_up(); + assert_eq!(defense.get_difficulty(), 500_000); + + defense.tighten_up(); + assert_eq!(defense.get_difficulty(), 5_000_000); + + defense.tighten_up(); + assert_eq!(defense.get_difficulty(), 5_000_000); + } + + #[test] + fn max_defense_works() { + let mut defense = get_defense(); + defense.max_defense(); + assert_eq!(defense.get_difficulty(), 5_000_000); + } + + #[test] + fn minimum_defense_works() { + let mut defense = get_defense(); + defense.min_defense(); + assert_eq!(defense.get_difficulty(), 50); + } + + #[test] + fn loosen_up_works() { + let mut defense = get_defense(); + defense.max_defense(); + + assert_eq!(defense.get_difficulty(), 5_000_000); + + defense.loosen_up(); + assert_eq!(defense.get_difficulty(), 500_000); + + defense.loosen_up(); + assert_eq!(defense.get_difficulty(), 50_000); + + defense.loosen_up(); + assert_eq!(defense.get_difficulty(), 5_000); + + defense.loosen_up(); + assert_eq!(defense.get_difficulty(), 50); + + defense.loosen_up(); + assert_eq!(defense.get_difficulty(), 50); + } + + // #[test] + // fn threshold_works() { + // let mut level = Levels::default(); + // + // assert_eq!(level.threshold(), Levels::One as usize); + // level.next(); + // assert_eq!(level.threshold(), Levels::Two as usize); + // level.next(); + // assert_eq!(level.threshold(), Levels::Three as usize); + // } + // + // #[test] + // fn difficulty_works() { + // let mut level = Levels::default(); + // + // assert_eq!(level.get_difficulty(), Levels::One as u32); + // level.next(); + // assert_eq!(level.get_difficulty(), Levels::Two as u32); + // level.next(); + // assert_eq!(level.get_difficulty(), 100_000); + // level.next(); + // assert_eq!(level.get_difficulty(), 1_000_000); + // } +}