Files
sqlpage--sqlpage/tests/index.rs
T
2024-03-23 13:13:29 +01:00

286 lines
9.7 KiB
Rust

use actix_web::{
body::MessageBody,
http::{self, header::ContentType, StatusCode},
test::{self, TestRequest},
};
use sqlpage::{app_config::AppConfig, webserver::http::main_handler, AppState};
#[actix_web::test]
async fn test_index_ok() {
let resp = req_path("/").await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let body = test::read_body(resp).await;
assert!(body.starts_with(b"<!DOCTYPE html>"));
// the body should contain the string "It works!" and should not contain the string "error"
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("It works !"));
assert!(!body.contains("error"));
}
#[actix_web::test]
async fn test_access_config_forbidden() {
let resp_result = req_path("/sqlpage/sqlpage.json").await;
assert!(resp_result.is_err(), "Accessing the config file should be forbidden, but we received a response: {resp_result:?}");
let resp = resp_result.unwrap_err().error_response();
assert_eq!(resp.status(), http::StatusCode::FORBIDDEN);
assert!(
String::from_utf8_lossy(&resp.into_body().try_into_bytes().unwrap())
.to_lowercase()
.contains("forbidden"),
);
}
#[actix_web::test]
async fn test_404() {
for f in [
"/does_not_exist.sql",
"/does_not_exist.html",
"/does_not_exist/",
] {
let resp_result = req_path(f).await;
let resp = resp_result.unwrap_err().error_response();
assert_eq!(resp.status(), http::StatusCode::NOT_FOUND, "{f} isnt 404");
}
}
#[actix_web::test]
async fn test_concurrent_requests() {
// send 32 requests (less than the default postgres pool size)
// at the same time to /tests/multiple_components.sql
let components = [
"table", "form", "card", "datagrid", "hero", "list", "timeline",
];
let app_data = make_app_data().await;
let reqs = (0..64)
.map(|i| {
let component = components[i % components.len()];
req_path_with_app_data(
format!("/tests/any_component.sql?component={}", component),
app_data.clone(),
)
})
.collect::<Vec<_>>();
// send all requests at the same time
let results = futures_util::future::join_all(reqs).await;
// check that all requests succeeded
for result in results.into_iter() {
let resp = result.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let body = test::read_body(resp).await;
assert!(
body.starts_with(b"<!DOCTYPE html>"),
"Expected html doctype"
);
// the body should contain the string "It works!" and should not contain the string "error"
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(
body.contains("It works !"),
"Expected to contain: It works !, but got: {body}"
);
assert!(!body.contains("error"));
}
}
#[actix_web::test]
async fn test_files() {
// Iterate over all the sql test files in the tests/ directory
let path = std::path::Path::new("tests/sql_test_files");
let app_data = make_app_data().await;
for entry in std::fs::read_dir(path).unwrap() {
let entry = entry.unwrap();
let test_file_path = entry.path();
let test_file_path_string = test_file_path.to_string_lossy().replace('\\', "/");
let stem = test_file_path.file_stem().unwrap().to_str().unwrap();
if test_file_path.extension().unwrap_or_default() != "sql" {
continue;
}
let req_str = format!("/{}?x=1", test_file_path_string);
let resp = req_path_with_app_data(&req_str, app_data.clone())
.await
.unwrap();
let body = test::read_body(resp).await;
assert!(
body.starts_with(b"<!DOCTYPE html>"),
"Response to {req_str} is not HTML"
);
// the body should contain the string "It works!" and should not contain the string "error"
let body = String::from_utf8(body.to_vec()).unwrap();
let lowercase_body = body.to_lowercase();
if stem.starts_with("it_works") {
assert!(
body.contains("It works !"),
"{req_str}\n{body}\nexpected to contain: It works !"
);
assert!(
!lowercase_body.contains("error"),
"{body}\nexpected to not contain: error"
);
} else if stem.starts_with("error_") {
let rest = stem.strip_prefix("error_").unwrap();
let expected_str = rest.replace('_', " ");
assert!(
lowercase_body.contains(&expected_str),
"{req_str}\n{body}\nexpected to contain: {expected_str}"
);
assert!(
lowercase_body.contains("error"),
"{req_str}\n{body}\nexpected to contain: error"
);
}
}
}
#[actix_web::test]
async fn test_file_upload() -> actix_web::Result<()> {
let req = get_request_to("/tests/upload_file_test.sql")
.await?
.insert_header(("content-type", "multipart/form-data; boundary=1234567890"))
.set_payload(
"--1234567890\r\n\
Content-Disposition: form-data; name=\"my_file\"; filename=\"testfile.txt\"\r\n\
Content-Type: text/plain\r\n\
\r\n\
Hello, world!\r\n\
--1234567890--\r\n",
)
.to_srv_request();
let resp = main_handler(req).await?;
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("Hello, world!"),
"{body_str}\nexpected to contain: Hello, world!"
);
Ok(())
}
#[actix_web::test]
async fn test_file_upload_too_large() -> actix_web::Result<()> {
// Files larger than 12345 bytes should be rejected as per the test_config
let req = get_request_to("/tests/upload_file_test.sql")
.await?
.insert_header(("content-type", "multipart/form-data; boundary=1234567890"))
.set_payload(
"--1234567890\r\n\
Content-Disposition: form-data; name=\"my_file\"; filename=\"testfile.txt\"\r\n\
Content-Type: text/plain\r\n\
\r\n\
"
.to_string()
+ "a".repeat(12346).as_str()
+ "\r\n\
--1234567890--\r\n",
)
.to_srv_request();
let err_str = main_handler(req)
.await
.expect_err("Expected an error response")
.to_string();
assert!(
err_str.to_ascii_lowercase().contains("max file size"),
"{err_str}\nexpected to contain: File too large"
);
Ok(())
}
#[actix_web::test]
async fn test_csv_upload() -> actix_web::Result<()> {
let req = get_request_to("/tests/upload_csv_test.sql")
.await?
.insert_header(("content-type", "multipart/form-data; boundary=1234567890"))
.set_payload(
"--1234567890\r\n\
Content-Disposition: form-data; name=\"people_file\"; filename=\"people.csv\"\r\n\
Content-Type: text/csv\r\n\
\r\n\
name,age\r\n\
Ophir,29\r\n\
Max,99\r\n\
--1234567890--\r\n",
)
.to_srv_request();
let resp = main_handler(req).await?;
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("Ophir is 29 years old"),
"{body_str}\nexpected to contain: Ophir is 29 years old"
);
Ok(())
}
#[actix_web::test]
/// `/sqlpage/migrations/0001_init.sql` should return a 403 Forbidden
async fn privileged_paths_are_not_accessible() {
let resp_result = req_path("/sqlpage/migrations/0001_init.sql").await;
assert!(
resp_result.is_err(),
"Accessing a migration file should be forbidden"
);
let resp = resp_result.unwrap_err().error_response();
assert_eq!(resp.status(), http::StatusCode::FORBIDDEN);
assert!(
String::from_utf8_lossy(&resp.into_body().try_into_bytes().unwrap())
.to_lowercase()
.contains("forbidden"),
);
}
async fn get_request_to(path: &str) -> actix_web::Result<TestRequest> {
let data = make_app_data().await;
Ok(test::TestRequest::get().uri(path).app_data(data))
}
async fn make_app_data() -> actix_web::web::Data<AppState> {
init_log();
let config = test_config();
let state = AppState::init(&config).await.unwrap();
actix_web::web::Data::new(state)
}
async fn req_path(
path: impl AsRef<str>,
) -> Result<actix_web::dev::ServiceResponse, actix_web::Error> {
let req = get_request_to(path.as_ref())
.await?
.insert_header(ContentType::plaintext())
.to_srv_request();
main_handler(req).await
}
async fn req_path_with_app_data(
path: impl AsRef<str>,
app_data: actix_web::web::Data<AppState>,
) -> Result<actix_web::dev::ServiceResponse, actix_web::Error> {
let req = test::TestRequest::get()
.uri(path.as_ref())
.app_data(app_data)
.to_srv_request();
main_handler(req).await
}
pub fn test_config() -> AppConfig {
let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite::memory:".to_string());
serde_json::from_str::<AppConfig>(&format!(
r#"{{
"database_url": "{}",
"database_connection_retries": 2,
"database_connection_acquire_timeout_seconds": 10,
"allow_exec": true,
"max_uploaded_file_size": 12345,
"listen_on": "111.111.111.111:1"
}}"#,
db_url
))
.unwrap()
}
fn init_log() {
let _ = env_logger::builder().is_test(true).try_init();
}