diff --git a/crack-core/src/commands/bf.rs b/crack-core/src/commands/bf.rs index 6354ef421..2b592e29d 100644 --- a/crack-core/src/commands/bf.rs +++ b/crack-core/src/commands/bf.rs @@ -33,11 +33,11 @@ pub async fn bf( /// Run a brainfk program. Program and input string maybe empty, /// no handling is done for invalid programs. -pub async fn bf_internal<'ctx>( - ctx: Context<'ctx>, +pub async fn bf_internal( + ctx: Context<'_>, program: String, input: String, -) -> Result, CrackedError> { +) -> Result, CrackedError> { tracing::info!("program: {program}, input: {input}"); let mut bf = BrainfuckProgram::new(program); diff --git a/crack-voting/Cargo.toml b/crack-voting/Cargo.toml index 6a0fdf5b1..4cccabad7 100644 --- a/crack-voting/Cargo.toml +++ b/crack-voting/Cargo.toml @@ -28,4 +28,16 @@ serde = { version = "1.0", features = ["derive"] } chrono = { version = "0.4", features = ["serde"] } tokio = { workspace = true } tracing = { workspace = true } -sqlx = { workspace = true } + +sqlx = { version = "0.7.4", default-features = false, features = [ + "runtime-tokio", + "postgres", + "migrate", +] } + +[dev-dependencies] +sqlx = { version = "0.7.4", default-features = false, features = [ + "runtime-tokio", + "postgres", + "migrate", +] } diff --git a/crack-voting/src/lib.rs b/crack-voting/src/lib.rs index 6fbb5185b..18976d46c 100644 --- a/crack-voting/src/lib.rs +++ b/crack-voting/src/lib.rs @@ -3,6 +3,34 @@ use lazy_static::lazy_static; use std::env; use warp::{body::BodyDeserializeError, http::StatusCode, path, reject, Filter, Rejection, Reply}; +const WEBHOOK_SECRET_DEFAULT: &str = "my-that-secret"; +const DATABASE_URL_DEFAULT: &str = "postgres://postgres:mysecretpassword@localhost/postgres"; + +lazy_static! { + static ref WEBHOOK_SECRET: String = + env::var("WEBHOOK_SECRET").unwrap_or(WEBHOOK_SECRET_DEFAULT.to_string()); + static ref DATABASE_URL: String = + env::var("DATABASE_URL").unwrap_or(DATABASE_URL_DEFAULT.to_string()); +} + +/// Struct to hold the context for the voting server. +#[derive(Debug, Clone)] +pub struct VotingContext { + pool: sqlx::PgPool, + secret: String, +} + +/// Implement the `VotingContext`. +impl VotingContext { + async fn new() -> Self { + let pool = sqlx::PgPool::connect(&DATABASE_URL) + .await + .expect("failed to connect to database"); + let secret = get_secret().to_string(); + VotingContext { pool, secret } + } +} + /// NewClass for the Webhook to store in the database. #[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, Clone, PartialEq, Eq)] pub struct CrackedWebhook { @@ -26,30 +54,27 @@ impl std::error::Error for Unauthorized {} /// Custom error type for unauthorized requests. #[derive(Debug)] -struct SQLX(sqlx::Error); +struct Sqlx(sqlx::Error); -impl warp::reject::Reject for SQLX {} +impl warp::reject::Reject for Sqlx {} -impl std::fmt::Display for SQLX { +impl std::fmt::Display for Sqlx { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0.to_string()) } } -impl std::error::Error for SQLX {} - -lazy_static! { - static ref WEBHOOK_SECRET: String = - env::var("WEBHOOK_SECRET").unwrap_or("missing secret".to_string()); -} - +impl std::error::Error for Sqlx {} /// Get the webhook secret from the environment. fn get_secret() -> &'static str { &WEBHOOK_SECRET } -/// -async fn write_webhook_to_db(pool: sqlx::PgPool, webhook: Webhook) -> Result<(), sqlx::Error> { +/// Write the received webhook to the database. +async fn write_webhook_to_db( + ctx: &'static VotingContext, + webhook: Webhook, +) -> Result<(), sqlx::Error> { sqlx::query!( r#"INSERT INTO vote_webhook (bot_id, user_id, kind, is_weekend, query, created_at) @@ -62,13 +87,13 @@ async fn write_webhook_to_db(pool: sqlx::PgPool, webhook: Webhook) -> Result<(), webhook.is_weekend, webhook.query, ) - .execute(&pool) + .execute(&ctx.pool) .await?; Ok(()) } /// Create a filter that checks the `Authorization` header against the secret. -fn header(secret: &'static str) -> impl Filter + Clone { +fn header(secret: &str) -> impl Filter + Clone + '_ { warp::header::("authorization") .and_then(move |val: String| async move { if val == secret { @@ -82,34 +107,36 @@ fn header(secret: &'static str) -> impl Filter .untuple_one() } -async fn process_webhook(hook: Webhook) -> Result { - let pool = sqlx::PgPool::connect(&env::var("DATABASE_URL").unwrap()) - .await - .unwrap(); - write_webhook_to_db(pool, hook.clone()) - .await - .map_err(SQLX)?; +/// Async function to process the received webhook. +async fn process_webhook( + ctx: &'static VotingContext, + hook: Webhook, +) -> Result { + write_webhook_to_db(ctx, hook.clone()).await.map_err(Sqlx)?; println!("{:?}", hook); Ok(warp::reply()) } /// Create a filter that handles the webhook. -async fn get_webhook() -> impl Filter + Clone { - let secret = get_secret(); +async fn get_webhook( + ctx: &'static VotingContext, +) -> impl Filter + Clone { println!("get_webhook"); warp::post() .and(path!("dbl" / "webhook")) - .and(header(secret)) + .and(header(&ctx.secret)) .and(warp::body::json()) - .and_then(move |hook: Webhook| async move { process_webhook(hook).await }) + .and_then(move |hook: Webhook| async move { process_webhook(ctx, hook).await }) .recover(custom_error) } /// Run the server. -pub async fn run() { - warp::serve(get_webhook().await) +pub async fn run() -> &'static VotingContext { + let ctx = Box::leak(Box::new(VotingContext::new().await)); + warp::serve(get_webhook(ctx).await) .run(([127, 0, 0, 1], 3030)) .await; + ctx } /// Custom error handling for the server. @@ -131,21 +158,27 @@ async fn custom_error(err: Rejection) -> Result { #[cfg(test)] mod test { - use super::*; - use warp::http::StatusCode; + use super::{get_secret, get_webhook}; + use super::{StatusCode, VotingContext, Webhook}; + + pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./test_migrations"); - #[tokio::test] + //#[sqlx::test(migrator = "MIGRATOR")] + #[sqlx::test] async fn test_bad_req() { + let ctx = Box::leak(Box::new(VotingContext::new().await)); let res = warp::test::request() .method("POST") .path("/dbl/webhook") - .reply(&get_webhook().await) + .reply(&get_webhook(ctx).await) .await; assert_eq!(res.status(), StatusCode::BAD_REQUEST); } - #[tokio::test] + //#[sqlx::test(migrator = "MIGRATOR")] + #[sqlx::test] async fn test_authorized() { + let ctx = Box::leak(Box::new(VotingContext::new().await)); let res = warp::test::request() .method("POST") .path("/dbl/webhook") @@ -157,7 +190,7 @@ mod test { is_weekend: false, query: Some("test".to_string()), }) - .reply(&get_webhook().await) + .reply(&get_webhook(ctx).await) .await; assert_eq!(res.status(), StatusCode::OK); } diff --git a/crack-voting/test_migrations/20240705083637_crack_voting.sql b/crack-voting/test_migrations/20240705083637_crack_voting.sql new file mode 100644 index 000000000..b7de37f24 --- /dev/null +++ b/crack-voting/test_migrations/20240705083637_crack_voting.sql @@ -0,0 +1,12 @@ +-- Add migration script here +CREATE TYPE WEBHOOK_KIND AS ENUM('upvote', 'test'); +CREATE TABLE vote_webhook ( + id SERIAL PRIMARY KEY, + bot_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + kind WEBHOOK_KIND NOT NULL, + is_weekend BOOLEAN NOT NULL, + query TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_vote_webhook_user_id FOREIGN KEY (user_id) REFERENCES "user"(id) +); \ No newline at end of file diff --git a/migrations/20240707101455_rename.sql b/migrations/20240707101455_rename.sql new file mode 100644 index 000000000..b79c19809 --- /dev/null +++ b/migrations/20240707101455_rename.sql @@ -0,0 +1,3 @@ +-- Add migration script here +ALTER TABLE IF EXISTS VOTE_WEBHOOK + RENAME TO vote_webhook; \ No newline at end of file