Skip to content

Commit

Permalink
Add tests and tracing for logs
Browse files Browse the repository at this point in the history
Also remove the fn `id` from the `Plugin` trait since it is not used.
  • Loading branch information
cmackenzie1 committed Feb 1, 2025
1 parent 7cb11d1 commit 57fdb5e
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 26 deletions.
10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ repository = "https://github.com/cmackenzie1/torii"
license = "MIT"

[workspace.dependencies]
thiserror = "2"
async-trait = "0.1"
regex = "1"
serde = "1"
thiserror = "2"
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
uuid = { version = "1", features = ["v4"] }

sqlx = { version = "0.8", features = [
"runtime-tokio",
"sqlite",
"sqlite", # TODO: Remove sqlite feature
"chrono",
"uuid",
] }
uuid = { version = "1", features = ["v4"] }
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ lint:

# Run tests using nextest
test:
@cargo nextest run
@cargo nextest run --no-fail-fast

# Run tests with coverage report
coverage:
@cargo llvm-cov --all-features
@cargo llvm-cov nextest --all-features

# Clean build artifacts
clean:
Expand Down
5 changes: 4 additions & 1 deletion torii-auth-email/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ license.workspace = true
[dependencies]
torii-core = { path = "../torii-core" }
async-trait.workspace = true
password-auth = { version = "1", features = ["argon2"] }
regex.workspace = true
sqlx.workspace = true
thiserror.workspace = true
password-auth = { version = "1", features = ["argon2"] }
tracing.workspace = true

[dev-dependencies]
tokio.workspace = true
tracing-subscriber.workspace = true
207 changes: 197 additions & 10 deletions torii-auth-email/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
//! Password is hashed using the `password_auth` crate using argon2.
mod migrations;

use std::any::TypeId;

use async_trait::async_trait;
use migrations::AddPasswordHashColumn;
use password_auth::{generate_hash, verify_password};
use regex::Regex;
use sqlx::pool::Pool;
use sqlx::sqlite::Sqlite;
use sqlx::Row;
Expand All @@ -31,10 +31,6 @@ impl EmailPasswordPlugin {

#[async_trait]
impl Plugin for EmailPasswordPlugin {
fn id(&self) -> TypeId {
TypeId::of::<EmailPasswordPlugin>()
}

fn name(&self) -> &'static str {
"email_password"
}
Expand All @@ -58,6 +54,13 @@ impl EmailPasswordPlugin {
let user_id = UserId::new_random();
let password_hash = generate_hash(password);

if !is_valid_email(email) {
return Err(Error::InvalidEmailFormat);
}
if !is_valid_password(password) {
return Err(Error::WeakPassword);
}

sqlx::query(
r#"
INSERT INTO users (id, email, password_hash) VALUES (?, ?, ?)
Expand All @@ -80,6 +83,13 @@ impl EmailPasswordPlugin {
.fetch_one(pool)
.await?;

tracing::info!(
user.id = %user.get::<String, _>("id"),
user.email = %user.get::<String, _>("email"),
user.name = %user.get::<String, _>("name"),
"Created user",
);

Ok(User {
id: user.get("id"),
name: user.get("name"),
Expand All @@ -101,17 +111,31 @@ impl EmailPasswordPlugin {
SELECT id, name, email, email_verified_at, password_hash, created_at, updated_at
FROM users
WHERE email = ?
LIMIT 1
"#,
)
.bind(email)
.fetch_one(pool)
.await?;
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => Error::UserNotFound,
_ => e.into(),
})?;

let stored_hash: String = row.get("password_hash");

if verify_password(password, &stored_hash).is_err() {
tracing::error!(email = email, "Invalid credentials");
return Err(Error::InvalidCredentials);
}

tracing::info!(
row.id = %row.get::<String, _>("id"),
row.email = %row.get::<String, _>("email"),
row.name = %row.get::<String, _>("name"),
"Logged in user",
);

Ok(User {
id: row.get("id"),
name: row.get("name"),
Expand All @@ -123,30 +147,86 @@ impl EmailPasswordPlugin {
}
}

/// Validate an email address.
fn is_valid_email(email: &str) -> bool {
let email_regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
email_regex.is_match(email)
}

/// Validate a password.
fn is_valid_password(password: &str) -> bool {
// TODO: Add more robust password validation
password.len() >= 8
}

#[cfg(test)]
mod tests {
use super::*;
use sqlx::SqlitePool;
use torii_core::PluginManager;

#[tokio::test]
async fn test_plugin_setup() -> Result<(), Error> {
async fn setup_plugin() -> Result<(PluginManager, Pool<Sqlite>), Error> {
let _ = tracing_subscriber::fmt().try_init(); // don't panic if this fails

let pool = SqlitePool::connect("sqlite::memory:")
.await
.expect("Failed to create pool");

let manager = PluginManager::new();
manager.register(EmailPasswordPlugin);

// Initialize and run migrations
manager.setup(&pool).await?;
manager.migrate(&pool).await?;

Ok((manager, pool))
}

#[test]
fn test_is_valid_email() {
// Valid email addresses
assert!(is_valid_email("test@example.com"));
assert!(is_valid_email("user.name@domain.co.uk"));
assert!(is_valid_email("user+tag@example.com"));
assert!(is_valid_email("123@domain.com"));

// Invalid email addresses
assert!(!is_valid_email(""));
assert!(!is_valid_email("not-an-email"));
assert!(!is_valid_email("@domain.com"));
assert!(!is_valid_email("user@"));
assert!(!is_valid_email("user@.com"));
assert!(!is_valid_email("user@domain"));
assert!(!is_valid_email("user name@domain.com"));
}

#[test]
fn test_is_valid_password() {
// Valid passwords (>= 8 characters)
assert!(is_valid_password("password123"));
assert!(is_valid_password("12345678"));
assert!(is_valid_password("abcdefghijklmnop"));

// Invalid passwords (< 8 characters)
assert!(!is_valid_password(""));
assert!(!is_valid_password("short"));
assert!(!is_valid_password("1234567"));
}

#[tokio::test]
async fn test_plugin_setup() -> Result<(), Error> {
let (_, pool) = setup_plugin().await?;

let count = sqlx::query("SELECT count(*) FROM users")
.fetch_one(&pool)
.await?;
assert_eq!(count.get::<i64, _>(0), 0);

Ok(())
}

#[tokio::test]
async fn test_create_user_and_login() -> Result<(), Error> {
let (manager, pool) = setup_plugin().await?;

let user = manager
.get_plugin::<EmailPasswordPlugin>()
.unwrap()
Expand All @@ -163,4 +243,111 @@ mod tests {

Ok(())
}

#[tokio::test]
async fn test_create_duplicate_user() -> Result<(), Error> {
let (manager, pool) = setup_plugin().await?;

let _ = manager
.get_plugin::<EmailPasswordPlugin>()
.unwrap()
.create_user(&pool, "test@example.com", "password")
.await?;

let user = manager
.get_plugin::<EmailPasswordPlugin>()
.unwrap()
.create_user(&pool, "test@example.com", "password")
.await;

assert!(user.is_err());

Ok(())
}

#[tokio::test]
async fn test_invalid_email_format() -> Result<(), Error> {
let (manager, pool) = setup_plugin().await?;

let result = manager
.get_plugin::<EmailPasswordPlugin>()
.expect("Plugin should exist")
.create_user(&pool, "not-an-email", "password")
.await;

assert!(result.is_err());

Ok(())
}

#[tokio::test]
async fn test_weak_password() -> Result<(), Error> {
let (manager, pool) = setup_plugin().await?;

let result = manager
.get_plugin::<EmailPasswordPlugin>()
.expect("Plugin should exist")
.create_user(&pool, "test@example.com", "123")
.await;

assert!(result.is_err());

Ok(())
}

#[tokio::test]
async fn test_incorrect_password_login() -> Result<(), Error> {
let (manager, pool) = setup_plugin().await?;

manager
.get_plugin::<EmailPasswordPlugin>()
.expect("Plugin should exist")
.create_user(&pool, "test@example.com", "password")
.await?;

let result = manager
.get_plugin::<EmailPasswordPlugin>()
.expect("Plugin should exist")
.login_user(&pool, "test@example.com", "wrong-password")
.await;

assert!(matches!(result, Err(Error::InvalidCredentials)));

Ok(())
}

#[tokio::test]
async fn test_nonexistent_user_login() -> Result<(), Error> {
let (manager, pool) = setup_plugin().await?;

let result = manager
.get_plugin::<EmailPasswordPlugin>()
.expect("Plugin should exist")
.login_user(&pool, "nonexistent@example.com", "password")
.await;

assert!(matches!(result, Err(Error::UserNotFound)));

Ok(())
}

#[tokio::test]
async fn test_sql_injection_attempt() -> Result<(), Error> {
let (manager, pool) = setup_plugin().await?;

let _ = manager
.get_plugin::<EmailPasswordPlugin>()
.expect("Plugin should exist")
.create_user(&pool, "test@example.com'; DROP TABLE users;--", "password")
.await
.expect_err("Should fail validation");

// Verify table still exists and no user was created
let count = sqlx::query("SELECT count(*) FROM users")
.fetch_one(&pool)
.await?;
assert_eq!(count.get::<i64, _>(0), 0);

Ok(())
}
}
5 changes: 0 additions & 5 deletions torii-auth-oidc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::any::TypeId;

use async_trait::async_trait;
use migrations::CreateOidcTables;
Expand Down Expand Up @@ -40,10 +39,6 @@ impl OIDCPlugin {

#[async_trait]
impl Plugin for OIDCPlugin {
fn id(&self) -> TypeId {
TypeId::of::<OIDCPlugin>()
}

fn name(&self) -> &'static str {
"oidc"
}
Expand Down
5 changes: 3 additions & 2 deletions torii-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ license.workspace = true

[dependencies]
async-trait.workspace = true
chrono = "0.4.39"
downcast-rs = "2.0.1"
sqlx.workspace = true
thiserror.workspace = true
tracing.workspace = true
uuid.workspace = true
chrono = "0.4.39"
downcast-rs = "2.0.1"

[features]
default = []
Loading

0 comments on commit 57fdb5e

Please sign in to comment.