Skip to content

Commit

Permalink
Add checks on create_tournament inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
SpartanPlume committed Feb 22, 2024
1 parent e982c8b commit a372f37
Show file tree
Hide file tree
Showing 11 changed files with 317 additions and 14 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
secrets/
.vscode
89 changes: 89 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ secrecy = { version = "0.8", features = ["serde"] }
tracing-actix-web = "0.7"
serde-aux = "4"
config-secret = "0.1"
unicode-segmentation = "1"

[dependencies.sqlx]
version = "0.7"
Expand All @@ -42,3 +43,5 @@ features = [
[dev-dependencies]
reqwest = { version = "0.11", features = ["json"] }
once_cell = "1"
claims = "0.7"
proptest = "1.4"
2 changes: 1 addition & 1 deletion migrations/20240215193936_create_tournaments_table.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CREATE TABLE tournaments(
CREATE TABLE IF NOT EXISTS tournaments(
id uuid NOT NULL,
PRIMARY KEY(id),
name TEXT NOT NULL UNIQUE,
Expand Down
7 changes: 7 additions & 0 deletions src/domain/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod tournament;
mod tournament_acronym;
mod tournament_name;

pub use tournament::Tournament;
pub use tournament_acronym::TournamentAcronym;
pub use tournament_name::TournamentName;
7 changes: 7 additions & 0 deletions src/domain/tournament.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use crate::domain::tournament_acronym::TournamentAcronym;
use crate::domain::tournament_name::TournamentName;

pub struct Tournament {
pub name: TournamentName,
pub acronym: TournamentAcronym,
}
69 changes: 69 additions & 0 deletions src/domain/tournament_acronym.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#[derive(Debug)]
pub struct TournamentAcronym(String);

impl TournamentAcronym {
pub fn parse(s: String) -> Result<TournamentAcronym, String> {
let s = s.trim().to_string();
if s.is_empty() {
return Err("Tournament acronym is empty".to_string());
}

if s.len() > 16 {
return Err("Tournament acronym is too long".to_string());
}

let allowed_special_characters = ['!', '#', '?', '@'];
if s.chars()
.any(|c| !c.is_alphanumeric() && !allowed_special_characters.contains(&c))
{
return Err("Tournament acronym contains invalid characters".to_string());
}

Ok(Self(s))
}
}

impl AsRef<str> for TournamentAcronym {
fn as_ref(&self) -> &str {
&self.0
}
}

#[cfg(test)]
mod tests {
use crate::domain::tournament_acronym::TournamentAcronym;

use claims::{assert_err, assert_ok};
use proptest::prelude::*;

#[test]
fn empty_acronym_is_invalid() {
let acronym = "".to_string();
assert_err!(TournamentAcronym::parse(acronym));
}

#[test]
fn acronym_with_only_whitespaces_is_invalid() {
let acronym = " ".to_string();
assert_err!(TournamentAcronym::parse(acronym));
}

#[test]
fn acronym_longer_than_16_is_invalid() {
let acronym = "a".repeat(17);
assert_err!(TournamentAcronym::parse(acronym));
}

#[test]
fn acronym_containing_invalid_character_is_invalid() {
let acronym = "/".to_string();
assert_err!(TournamentAcronym::parse(acronym));
}

proptest! {
#[test]
fn acronym_containing_valid_characters_is_valid(acronym in "[a-z][A-Z][0-9]!#?@") {
assert_ok!(TournamentAcronym::parse(acronym));
}
}
}
75 changes: 75 additions & 0 deletions src/domain/tournament_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use unicode_segmentation::UnicodeSegmentation;

#[derive(Debug)]
pub struct TournamentName(String);

impl TournamentName {
pub fn parse(s: String) -> Result<TournamentName, String> {
let s = s.trim().to_string();
if s.is_empty() {
return Err("Tournament name is empty".to_string());
}

if s.graphemes(true).count() > 128 {
return Err("Tournament name is too long".to_string());
}

let forbidden_characters = ['\\', '"'];
if s.chars().any(|c| forbidden_characters.contains(&c)) {
return Err("Tournament name contains invalid characters".to_string());
}

Ok(Self(s))
}
}

impl AsRef<str> for TournamentName {
fn as_ref(&self) -> &str {
&self.0
}
}

#[cfg(test)]
mod tests {
use crate::domain::tournament_name::TournamentName;

use claims::{assert_err, assert_ok};

#[test]
fn empty_name_is_invalid() {
let name = "".to_string();
assert_err!(TournamentName::parse(name));
}

#[test]
fn name_with_only_whitespaces_is_invalid() {
let name = " ".to_string();
assert_err!(TournamentName::parse(name));
}

#[test]
fn name_with_128_graphemes_is_valid() {
let name = "ɑ".repeat(128);
assert_ok!(TournamentName::parse(name));
}

#[test]
fn name_longer_than_128_is_invalid() {
let name = "a".repeat(129);
assert_err!(TournamentName::parse(name));
}

#[test]
fn name_containing_invalid_characters_is_invalid() {
for name in &['\\', '"'] {
let name = name.to_string();
assert_err!(TournamentName::parse(name));
}
}

#[test]
fn name_containing_valid_characters_is_valid() {
let name = "Tournament name".to_string();
assert_ok!(TournamentName::parse(name));
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod configuration;
pub mod domain;
pub mod routes;
pub mod secret_env;
pub mod startup;
Expand Down
28 changes: 23 additions & 5 deletions src/routes/tournaments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,37 @@ use serde::Deserialize;
use sqlx::PgPool;
use uuid::Uuid;

use crate::domain::Tournament;
use crate::domain::TournamentAcronym;
use crate::domain::TournamentName;

#[derive(Deserialize)]
struct Tournament {
struct BodyData {
name: String,
acronym: String,
}

impl TryFrom<BodyData> for Tournament {
type Error = String;

fn try_from(value: BodyData) -> Result<Self, Self::Error> {
let name = TournamentName::parse(value.name)?;
let acronym = TournamentAcronym::parse(value.acronym)?;
Ok(Self { name, acronym })
}
}

#[allow(clippy::async_yields_async)]
#[post("/tournaments")]
#[tracing::instrument(skip_all, fields(%tournament.name))]
#[tracing::instrument(skip_all, fields(%body_data.name))]
async fn create_tournament(
tournament: web::Json<Tournament>,
body_data: web::Json<BodyData>,
db_pool: web::Data<PgPool>,
) -> impl Responder {
let tournament = match body_data.0.try_into() {
Ok(tournament) => tournament,
Err(_) => return HttpResponse::BadRequest(),
};
match insert_tournament_in_db(&tournament, &db_pool).await {
Ok(_) => HttpResponse::Ok(),
Err(_) => HttpResponse::InternalServerError(),
Expand All @@ -34,8 +52,8 @@ async fn insert_tournament_in_db(
VALUES ($1, $2, $3, $4, $5)
"#,
Uuid::new_v4(),
tournament.name,
tournament.acronym,
tournament.name.as_ref(),
tournament.acronym.as_ref(),
Utc::now(),
Utc::now()
)
Expand Down
Loading

0 comments on commit a372f37

Please sign in to comment.