diff --git a/.env b/.env new file mode 100644 index 0000000..df3e56d --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +DATABASE_URL=postgresql://user:password@127.0.0.1:8001/godsaeng +REDIS_URL=redis://127.0.0.1:6379 +SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0ded3fb..b245b5f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -44,6 +44,15 @@ jobs: test: name: ✅ Test Suite runs-on: ubuntu-latest + services: + postgres: + image: postgres:14 + ports: + - 8001:5432 + redis: + image: redis:7 + ports: + - 6379:6379 steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 @@ -51,6 +60,20 @@ jobs: toolchain: stable override: true - uses: Swatinem/rust-cache@v1 + with: + key: sqlx-0.6.2 + - name: Install sqlx-cli + run: + cargo install sqlx-cli + --version=0.6.2 + --features rustls,postgres + --no-default-features + --locked + - name: Migrate database + run: | + sudo apt-get install libpq-dev -y + sqlx database create + sqlx migrate run - name: Install cargo-nextest uses: baptiste0928/cargo-install@v1 with: diff --git a/.gitignore b/.gitignore index c778313..50c8301 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,4 @@ Cargo.lock # These are backup files generated by rustfmt -**/*.rs.bk - -## env file -.env \ No newline at end of file +**/*.rs.bk \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index b0a8a23..8a728f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,8 @@ dotenv = "0.15.0" serde = { version = "1.0.152", features = ["derive"] } sqlx = { version = "0.6.2", features = ["runtime-async-std-native-tls", "postgres", "chrono"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } + +[dev-dependencies] +reqwest = { version = "0.11.14", features = ["json", "cookies"] } +serde_json = "1.0.91" +uuid = { version = "1", features = ["v4"] } diff --git a/tests/event_check.rs b/tests/event_check.rs new file mode 100644 index 0000000..a04917e --- /dev/null +++ b/tests/event_check.rs @@ -0,0 +1,166 @@ +use reqwest::{Client, Response}; +use serde::Deserialize; +use serde_json::{Map, Number, Value}; + +mod helper; +use helper::{login, test_run, TestApp}; + +#[derive(Deserialize)] +struct IdRow { + pub id: i32, +} + +async fn create_event( + app: &TestApp, + client: &Client, + note: &str, + event_date: &str, +) -> Result { + let mut map = Map::new(); + map.insert("note".to_string(), Value::String(note.to_string())); + map.insert( + "event_date".to_string(), + Value::String(event_date.to_string()), + ); + let response = client + .post(&format!("{}/event", &app.address)) + .json(&map) + .send() + .await + .expect("Failed to execute request."); + Ok(response) +} + +async fn patch_event( + app: &TestApp, + client: &Client, + id: i32, + new_note: &str, + new_event_date: &str, +) -> Result { + let mut map = Map::new(); + map.insert("id".to_string(), Value::Number(Number::from(id))); + map.insert("new_note".to_string(), Value::String(new_note.to_string())); + map.insert( + "new_event_date".to_string(), + Value::String(new_event_date.to_string()), + ); + let response = client + .patch(&format!("{}/event", &app.address)) + .json(&map) + .send() + .await + .expect("Failed to execute request."); + Ok(response) +} + +async fn delete_event(app: &TestApp, client: &Client, id: i32) -> Result { + let mut map = Map::new(); + map.insert("id".to_string(), Value::Number(Number::from(id))); + let response = client + .delete(&format!("{}/event", &app.address)) + .json(&map) + .send() + .await + .expect("Failed to execute request."); + Ok(response) +} + +#[tokio::test] +async fn create_event_works() { + let app = test_run().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("Failed to create Client"); + + login(&app, &client, "admin", "secret") + .await + .expect("Failed to login as admin"); + + let response: Response = create_event(&app, &client, "test_note", "2022-01-01") + .await + .expect("Failed to create event."); + assert!(response.status().is_success()); + + let ret: IdRow = response.json().await.expect("Failed to get the json"); + assert_eq!(ret.id, 1) +} + +#[tokio::test] +async fn patch_event_works() { + let app = test_run().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("Failed to create Client"); + + login(&app, &client, "admin", "secret") + .await + .expect("Failed to login as admin"); + + let event_id = { + let response: Response = create_event(&app, &client, "test_note", "2022-01-01") + .await + .expect("Failed to create event."); + let ret: IdRow = response.json().await.expect("Failed to get the json"); + ret.id + }; + assert_eq!(event_id, 1); + + let response: Response = patch_event(&app, &client, event_id, "new_note", "2023-01-01") + .await + .expect("Failed to patch event"); + assert!(response.status().is_success()); +} + +#[tokio::test] +async fn patch_event_returns_a_400_for_invalid_id() { + let app = test_run().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("Failed to create Client"); + + login(&app, &client, "admin", "secret") + .await + .expect("Failed to login as admin"); + let event_id = { + let response: Response = create_event(&app, &client, "test_note", "2022-01-01") + .await + .expect("Failed to create event."); + let ret: IdRow = response.json().await.expect("Failed to get the json"); + ret.id + }; + assert_eq!(event_id, 1); + + let response: Response = patch_event(&app, &client, event_id + 1, "new_note", "2023-01-01") + .await + .expect("Failed to patch event"); + assert_eq!(400, response.status().as_u16()); +} + +#[tokio::test] +async fn delete_event_works() { + let app = test_run().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("Failed to create Client"); + + login(&app, &client, "admin", "secret") + .await + .expect("Failed to login as admin"); + let event_id = { + let response: Response = create_event(&app, &client, "test_note", "2022-01-01") + .await + .expect("Failed to create event."); + let ret: IdRow = response.json().await.expect("Failed to get the json"); + ret.id + }; + let response: Response = delete_event(&app, &client, event_id) + .await + .expect("Failed to patch event"); + + assert!(response.status().is_success()); +} diff --git a/tests/helper.rs b/tests/helper.rs new file mode 100644 index 0000000..4ada618 --- /dev/null +++ b/tests/helper.rs @@ -0,0 +1,77 @@ +use actix_session::storage::RedisSessionStore; +use dotenv::dotenv; +use godsaeng_backend::runner::run; +use reqwest::{Client, Response}; +use serde_json::{Map, Value}; +use sqlx::{Connection, Executor, PgConnection, PgPool}; +use std::net::TcpListener; + +use uuid::Uuid; + +pub struct TestApp { + pub address: String, + pub db_pool: PgPool, +} + +pub async fn test_run() -> TestApp { + let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port"); + let port = listener.local_addr().unwrap().port(); + let address = format!("http://127.0.0.1:{port}"); + + dotenv().ok(); + let db_url = std::env::var("TEST_DATABASE_URL").expect("DATABASE_URL must be set"); + let test_database = format!("{db_url}/postgres"); + + let mut connection = PgConnection::connect(&test_database) + .await + .expect("Failed to connect to Postgres"); + + let database_name = Uuid::new_v4().to_string(); + + connection + .execute(&*format!(r#"CREATE DATABASE "{database_name}";"#)) + .await + .expect("Failed to create database."); + + let test_database_url = format!("{db_url}/{database_name}"); + println!("{test_database_url}"); + let connection_pool = PgPool::connect(&test_database_url) + .await + .expect("Failed to connect to Postgres"); + + sqlx::migrate!("./migrations") + .run(&connection_pool) + .await + .expect("Failed to migrate the database"); + let store = { + let redis_url = std::env::var("REDIS_URL").expect("REDI must be set"); + RedisSessionStore::new(redis_url).await.unwrap() + }; + let secret_key = std::env::var("SECRET_KEY").expect("SECRET_KEY must be set"); + let server = + run(listener, connection_pool.clone(), store, secret_key).expect("Failed to bind address"); + tokio::spawn(server); + + TestApp { + address, + db_pool: connection_pool, + } +} + +pub async fn login( + app: &TestApp, + client: &Client, + name: &str, + password: &str, +) -> Result { + let mut map = Map::new(); + map.insert("name".to_string(), Value::String(name.to_string())); + map.insert("password".to_string(), Value::String(password.to_string())); + let response = client + .post(&format!("{}/login", &app.address)) + .json(&map) + .send() + .await + .expect("Failed to execute request."); + Ok(response) +} diff --git a/tests/user_check.rs b/tests/user_check.rs new file mode 100644 index 0000000..c7dbb28 --- /dev/null +++ b/tests/user_check.rs @@ -0,0 +1,200 @@ +use reqwest::{Client, Response}; +use serde_json::{Map, Value}; + +mod helper; +use helper::{login, test_run, TestApp}; + +async fn create_user( + app: &TestApp, + client: &Client, + name: &str, + password: &str, +) -> Result { + let mut map = Map::new(); + map.insert("name".to_string(), Value::String(name.to_string())); + map.insert("password".to_string(), Value::String(password.to_string())); + + let response = client + .post(&format!("{}/user", &app.address)) + .json(&map) + .send() + .await + .expect("Failed to execute request."); + Ok(response) +} + +async fn patch_user( + app: &TestApp, + client: &Client, + name: &str, + password: &str, + new_name: &str, + new_password: &str, +) -> Result { + let mut map = Map::new(); + map.insert("name".to_string(), Value::String(name.to_string())); + map.insert("password".to_string(), Value::String(password.to_string())); + map.insert("new_name".to_string(), Value::String(new_name.to_string())); + map.insert( + "new_password".to_string(), + Value::String(new_password.to_string()), + ); + + let response = client + .patch(&format!("{}/user", &app.address)) + .json(&map) + .send() + .await + .expect("Failed to execute request."); + Ok(response) +} + +async fn delete_user( + app: &TestApp, + client: &Client, + name: &str, + password: &str, +) -> Result { + let mut map = Map::new(); + map.insert("name".to_string(), Value::String(name.to_string())); + map.insert("password".to_string(), Value::String(password.to_string())); + + let response = client + .delete(&format!("{}/user", &app.address)) + .json(&map) + .send() + .await + .expect("Failed to execute request."); + Ok(response) +} + +#[tokio::test] +async fn create_user_works() { + let app = test_run().await; + let client = reqwest::Client::new(); + let response: Response = create_user(&app, &client, "test_user", "test_password") + .await + .expect("Failed to create user"); + assert!(response.status().is_success()); +} + +#[tokio::test] +async fn create_user_returns_a_400_for_duplicated_id() { + let app = test_run().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("Failed to create Client"); + let response: Response = create_user(&app, &client, "admin", "test_password") + .await + .expect("Failed to create user"); + + assert_eq!(400, response.status().as_u16()); +} + +#[tokio::test] +async fn create_user_returns_a_500_for_long_id() { + let app = test_run().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("Failed to create Client"); + let response: Response = create_user( + &app, + &client, + "test_user_loooooooooooooooooooooooooooooong_id", + "test_password", + ) + .await + .expect("Failed to create user"); + + assert_eq!(500, response.status().as_u16()); +} + +#[tokio::test] +async fn patch_user_works() { + let app = test_run().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("Failed to create Client"); + + login(&app, &client, "admin", "secret") + .await + .expect("Failed to login as admin"); + + let response: Response = patch_user( + &app, + &client, + "admin", + "secret", + "test_user_new", + "test_password_new", + ) + .await + .expect("Failed to create user"); + + assert!(response.status().is_success()); +} + +#[tokio::test] +async fn patch_user_returns_a_400_for_duplicated_id() { + let app = test_run().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("Failed to create Client"); + + login(&app, &client, "admin", "secret") + .await + .expect("Failed to login as admin"); + + let response: Response = patch_user( + &app, + &client, + "admin", + "secret", + "admin", + "test_password_new", + ) + .await + .expect("Failed to create user"); + + assert_eq!(400, response.status().as_u16()); +} + +#[tokio::test] +async fn delete_user_works() { + let app = test_run().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("Failed to create Client"); + + login(&app, &client, "admin", "secret") + .await + .expect("Failed to login as admin"); + + let response: Response = delete_user(&app, &client, "admin", "secret") + .await + .expect("Failed to create user"); + assert!(response.status().is_success()); +} + +#[tokio::test] +async fn delete_user_returns_a_401_for_invalid_password() { + let app = test_run().await; + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("Failed to create Client"); + + login(&app, &client, "admin", "secret") + .await + .expect("Failed to login as admin"); + + let response: Response = delete_user(&app, &client, "admin", "secret_ss") + .await + .expect("Failed to create user"); + assert_eq!(401, response.status().as_u16()); +}