reorganize tests: better structure

This commit is contained in:
lovasoa
2025-05-25 23:20:31 +02:00
parent 0695cfe668
commit 5cbd12fcfd
35 changed files with 921 additions and 995 deletions
+85
View File
@@ -0,0 +1,85 @@
use actix_web::{
body::MessageBody,
http::{self, StatusCode},
test,
};
use sqlpage::webserver::http::main_handler;
use crate::common::{get_request_to, req_path};
#[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>"));
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_404_fallback() {
for f in [
"/tests/does_not_exist.sql",
"/tests/does_not_exist.html",
"/tests/does_not_exist/",
] {
let resp_result = req_path(f).await;
let resp = resp_result.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK, "{f} isnt 200");
let body = test::read_body(resp).await;
assert!(body.starts_with(b"<!DOCTYPE html>"));
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("But the "));
assert!(body.contains("404.sql"));
assert!(body.contains("file saved the day!"));
assert!(!body.contains("error"));
}
}
#[actix_web::test]
async fn test_static_files() {
let resp = req_path("/tests/it_works.txt").await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let body = test::read_body(resp).await;
assert_eq!(&body, &b"It works !"[..]);
}
#[actix_web::test]
async fn test_spaces_in_file_names() {
let resp = req_path("/tests/core/spaces%20in%20file%20name.sql")
.await
.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(body_str.contains("It works !"), "{body_str}");
}
+104
View File
@@ -0,0 +1,104 @@
use std::{collections::HashMap, path::PathBuf, time::Duration};
use actix_web::{
http::header::ContentType,
test::{self, TestRequest},
web::Data,
};
use sqlpage::{
app_config::{test_database_url, AppConfig},
webserver::{
http::{form_config, main_handler, payload_config},
},
AppState,
};
pub async fn get_request_to_with_data(
path: &str,
data: Data<AppState>,
) -> actix_web::Result<TestRequest> {
Ok(test::TestRequest::get()
.uri(path)
.insert_header(ContentType::plaintext())
.app_data(payload_config(&data))
.app_data(form_config(&data))
.app_data(data))
}
pub async fn get_request_to(path: &str) -> actix_web::Result<TestRequest> {
let data = make_app_data().await;
get_request_to_with_data(path, data).await
}
pub async fn make_app_data_from_config(config: AppConfig) -> Data<AppState> {
let state = AppState::init(&config).await.unwrap();
Data::new(state)
}
pub async fn make_app_data() -> Data<AppState> {
init_log();
let config = test_config();
make_app_data_from_config(config).await
}
pub 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?.to_srv_request();
main_handler(req).await
}
pub async fn srv_req_path_with_app_data(
path: impl AsRef<str>,
app_data: Data<AppState>,
) -> actix_web::dev::ServiceRequest {
test::TestRequest::get()
.uri(path.as_ref())
.app_data(app_data)
.insert_header(("cookie", "test_cook=123"))
.insert_header(("authorization", "Basic dGVzdDp0ZXN0")) // test:test
.to_srv_request()
}
const REQ_TIMEOUT: Duration = Duration::from_secs(8);
pub async fn req_path_with_app_data(
path: impl AsRef<str>,
app_data: Data<AppState>,
) -> anyhow::Result<actix_web::dev::ServiceResponse> {
let path = path.as_ref();
let req = srv_req_path_with_app_data(path, app_data).await;
let resp = tokio::time::timeout(REQ_TIMEOUT, main_handler(req))
.await
.map_err(|e| anyhow::anyhow!("Request to {path} timed out: {e}"))?
.map_err(|e| {
anyhow::anyhow!(
"Request to {path} failed with status {}: {e:#}",
e.as_response_error().status_code()
)
})?;
Ok(resp)
}
pub fn test_config() -> AppConfig {
let db_url = test_database_url();
serde_json::from_str::<AppConfig>(&format!(
r#"{{
"database_url": "{db_url}",
"max_database_pool_connections": 1,
"database_connection_retries": 3,
"database_connection_acquire_timeout_seconds": 15,
"allow_exec": true,
"max_uploaded_file_size": 123456,
"listen_on": "111.111.111.111:1",
"system_root_ca_certificates" : false
}}"#
))
.unwrap()
}
pub fn init_log() {
let _ = env_logger::builder()
.parse_default_env()
.is_test(true)
.try_init();
}
+28
View File
@@ -0,0 +1,28 @@
use actix_web::{
http::StatusCode,
test,
};
use sqlpage::webserver::http::main_handler;
use crate::common::get_request_to;
#[actix_web::test]
async fn test_overwrite_variable() -> actix_web::Result<()> {
let req = get_request_to("/tests/sql_test_files/it_works_set_variable.sql")
.await?
.set_form(std::collections::HashMap::<&str, &str>::from_iter([(
"what_does_it_do",
"does not overwrite variables",
)]))
.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("It works !"),
"{body_str}\nexpected to contain: It works !"
);
Ok(())
}
+199
View File
@@ -0,0 +1,199 @@
use actix_web::{
http::{self, StatusCode},
test,
};
use sqlpage::{
webserver,
AppState,
};
use sqlx::Executor as _;
use crate::common::{make_app_data_from_config, req_path, req_path_with_app_data, test_config};
#[actix_web::test]
async fn test_concurrent_requests() {
let components = [
"table", "form", "card", "datagrid", "hero", "list", "timeline",
];
let app_data = make_app_data_from_config(test_config()).await;
let reqs = (0..64)
.map(|i| {
let component = components[i % components.len()];
req_path_with_app_data(
format!("/tests/components/any_component.sql?component={component}"),
app_data.clone(),
)
})
.collect::<Vec<_>>();
let results = futures_util::future::join_all(reqs).await;
for result in results.into_iter() {
let resp = result.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
assert!(
body.starts_with(b"<!DOCTYPE html>"),
"Expected html doctype"
);
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_routing_with_db_fs() {
let mut config = test_config();
if config.database_url.contains("memory") {
return;
}
config.site_prefix = "/prefix/".to_string();
let state = AppState::init(&config).await.unwrap();
let create_table_sql = sqlpage::filesystem::DbFsQueries::get_create_table_sql(state.db.connection.any_kind());
state
.db
.connection
.execute(format!("DROP TABLE IF EXISTS sqlpage_files; {create_table_sql}").as_ref())
.await
.unwrap();
let insert_sql = match state.db.connection.any_kind() {
sqlx::any::AnyKind::Mssql => "INSERT INTO sqlpage_files(path, contents) VALUES ('on_db.sql', CONVERT(VARBINARY(MAX), 'select ''text'' as component, ''Hi from db !'' AS contents;'))",
_ => "INSERT INTO sqlpage_files(path, contents) VALUES ('on_db.sql', 'select ''text'' as component, ''Hi from db !'' AS contents;')"
};
state.db.connection.execute(insert_sql).await.unwrap();
let state = AppState::init(&config).await.unwrap();
let app_data = actix_web::web::Data::new(state);
let resp = req_path_with_app_data("/prefix/on_db.sql", app_data.clone())
.await
.unwrap();
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("Hi from db !"),
"{body_str}\nexpected to contain: Hi from db !"
);
}
#[actix_web::test]
async fn test_routing_with_prefix() {
let mut config = test_config();
config.site_prefix = "/prefix/".to_string();
let state = AppState::init(&config).await.unwrap();
let app_data = actix_web::web::Data::new(state);
let resp = req_path_with_app_data(
"/prefix/tests/sql_test_files/it_works_simple.sql",
app_data.clone(),
)
.await
.unwrap();
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("It works !"),
"{body_str}\nexpected to contain: It works !"
);
assert!(
body_str.contains("href=\"/prefix/"),
"{body_str}\nexpected to contain links with site prefix"
);
let resp = req_path_with_app_data("/prefix/nonexistent.sql", app_data.clone())
.await
.expect_err("Expected 404 error")
.to_string();
assert!(
resp.contains("404"),
"Response should contain \"404\", but got:\n{resp}"
);
let resp = req_path_with_app_data("/prefix/sqlpage/migrations/0001_init.sql", app_data.clone())
.await
.expect_err("Expected forbidden error")
.to_string();
assert!(resp.to_lowercase().contains("forbidden"), "{resp}");
let resp = req_path_with_app_data("/tests/sql_test_files/it_works_simple.sql", app_data)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::MOVED_PERMANENTLY);
let location = resp
.headers()
.get("location")
.expect("location header should be present");
assert_eq!(location.to_str().unwrap(), "/prefix/");
}
#[actix_web::test]
async fn test_hidden_files() {
let resp_result = req_path("/tests/core/.hidden.sql").await;
assert!(
resp_result.is_err(),
"Accessing a hidden file should be forbidden, but received success: {resp_result:?}"
);
let resp = resp_result.unwrap_err().error_response();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
let srv_resp = actix_web::test::TestRequest::default().to_srv_response(resp);
let body = test::read_body(srv_resp).await;
assert!(
String::from_utf8_lossy(&body)
.to_lowercase()
.contains("forbidden"),
);
}
#[actix_web::test]
async fn test_official_website_documentation() {
let app_data = make_app_data_for_official_website().await;
let resp = req_path_with_app_data("/component.sql?component=button", app_data)
.await
.unwrap_or_else(|e| {
panic!("Failed to get response for /component.sql?component=button: {e}")
});
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(r#"<button type="submit" form="poem" formaction="?action"#),
"{body_str}\nexpected to contain a button with formaction"
);
}
#[actix_web::test]
async fn test_official_website_basic_auth_example() {
let resp = req_path_with_app_data(
"/examples/authentication/basic_auth.sql",
make_app_data_for_official_website().await,
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("not authorized"),
"{body_str}\nexpected to contain Unauthorized"
);
}
async fn make_app_data_for_official_website() -> actix_web::web::Data<AppState> {
crate::common::init_log();
let config_path = std::path::Path::new("examples/official-site/sqlpage");
let mut app_config = sqlpage::app_config::load_from_directory(config_path).unwrap();
app_config.web_root = std::path::PathBuf::from("examples/official-site");
app_config.database_url = "sqlite::memory:".to_string();
let app_state = make_app_data_from_config(app_config.clone()).await;
webserver::database::migrations::apply(&app_config, &app_state.db)
.await
.unwrap();
app_state
}
+82
View File
@@ -0,0 +1,82 @@
use actix_web::{
http::{header, StatusCode},
test,
};
use sqlpage::webserver::http::main_handler;
use crate::common::get_request_to;
#[actix_web::test]
async fn test_json_body() -> actix_web::Result<()> {
let req = get_request_to("/tests/data_formats/json_data.sql")
.await?
.to_srv_request();
let resp = main_handler(req).await?;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/json"
);
let body = test::read_body(resp).await;
let body_json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(
body_json,
serde_json::json!([{"message": "It works!"}, {"cool": "cool"}])
);
Ok(())
}
#[actix_web::test]
async fn test_csv_body() -> actix_web::Result<()> {
let req = get_request_to("/tests/data_formats/csv_data.sql")
.await?
.to_srv_request();
let resp = main_handler(req).await?;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/csv; charset=utf-8"
);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(
body_str,
"id;msg\n0;Hello World !\n1;\"Tu gères ';' et '\"\"' ?\"\n"
);
Ok(())
}
#[actix_web::test]
async fn test_json_columns() {
let app_data = crate::common::make_app_data().await;
if !matches!(
app_data.db.to_string().to_lowercase().as_str(),
"postgres" | "sqlite"
) {
log::info!("Skipping test_json_columns on database {}", app_data.db);
return;
}
let resp_result = crate::common::req_path("/tests/data_formats/json_columns.sql").await;
let resp = resp_result.expect("Failed to request /tests/data_formats/json_columns.sql");
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
let body_html_escaped = body_str.replace("&quot;", "\"");
assert!(
!body_html_escaped.contains("error"),
"the request should not have failed, in: {body_html_escaped}"
);
assert!(body_html_escaped.contains("1GB Database"));
assert!(body_html_escaped.contains("Priority Support"));
assert!(
!body_html_escaped.contains("\"description\""),
"the json should have been parsed, not returned as a string, in: {body_html_escaped}"
);
assert!(
!body_html_escaped.contains("{"),
"the json should have been parsed, not returned as a string, in: {body_html_escaped}"
);
}
+25
View File
@@ -0,0 +1,25 @@
use actix_web::{
http::StatusCode,
test,
};
use sqlpage::webserver::http::main_handler;
use crate::common::req_path;
#[actix_web::test]
async fn test_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, but received success: {resp_result:?}"
);
let resp = resp_result.unwrap_err().error_response();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
let srv_resp = actix_web::test::TestRequest::default().to_srv_response(resp);
let body = test::read_body(srv_resp).await;
assert!(
String::from_utf8_lossy(&body)
.to_lowercase()
.contains("forbidden"),
);
}
-990
View File
@@ -1,990 +0,0 @@
use std::{collections::HashMap, path::PathBuf};
use actix_web::{
body::MessageBody,
dev::{fn_service, ServerHandle, ServiceRequest, ServiceResponse},
http::{
self,
header::{self, ContentType},
StatusCode,
},
test::{self, TestRequest},
HttpResponse,
};
use sqlpage::{
app_config::{test_database_url, AppConfig},
filesystem::DbFsQueries,
webserver::{
self,
http::{form_config, main_handler, payload_config},
},
AppState,
};
use sqlx::Executor as _;
#[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_404_fallback() {
for f in [
"/tests/does_not_exist.sql",
"/tests/does_not_exist.html",
"/tests/does_not_exist/",
] {
let resp_result = req_path(f).await;
let resp = resp_result.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK, "{f} isnt 200");
let body = test::read_body(resp).await;
assert!(body.starts_with(b"<!DOCTYPE html>"));
// the body should contain our happy string, but not the string "error"
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("But the "));
assert!(body.contains("404.sql"));
assert!(body.contains("file saved the day!"));
assert!(!body.contains("error"));
}
}
#[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"));
}
}
fn start_echo_server() -> ServerHandle {
async fn echo_server(mut r: ServiceRequest) -> actix_web::Result<ServiceResponse> {
use std::io::Write;
let mut f = Vec::new();
write!(f, "{} {}", r.method(), r.uri()).unwrap();
let mut sorted_headers = r.headers().into_iter().collect::<Vec<_>>();
sorted_headers.sort_by_key(|(k, _)| k.as_str());
for (k, v) in sorted_headers {
if k.as_str().eq_ignore_ascii_case("date") {
continue;
}
write!(f, "|{k}: ").unwrap();
f.extend_from_slice(v.as_bytes());
}
f.push(b'|');
f.extend_from_slice(&r.extract::<actix_web::web::Bytes>().await?);
let resp = HttpResponse::Ok()
.insert_header((header::DATE, "Mon, 24 Feb 2025 12:00:00 GMT"))
.insert_header((header::CONTENT_TYPE, "text/plain"))
.body(f);
Ok(r.into_response(resp))
}
let server = actix_web::HttpServer::new(move || {
actix_web::App::new().default_service(fn_service(echo_server))
})
.bind("localhost:62802")
.unwrap()
.shutdown_timeout(5) // shutdown timeout
.run();
let handle = server.handle();
tokio::spawn(server);
handle
}
#[actix_web::test]
async fn test_files() {
// start a dummy server that test files can query
let echo_server = start_echo_server();
// 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;
}
if stem.contains(&format!("no{}", app_data.db.to_string().to_lowercase())) {
// skipping because the test does not support the database we are using
log::info!(
"Skipping test file {} on database {}",
test_file_path_string,
app_data.db
);
continue;
}
if test_file_path_string.contains(&format!("no{}", app_data.db.to_string().to_lowercase()))
{
// skipping because the test does not support the database
continue;
}
let req_str = format!("/{test_file_path_string}?x=1");
let resp = req_path_with_app_data(&req_str, app_data.clone())
.await
.unwrap_or_else(|e| panic!("Failed to get response for {req_str}: {e}"));
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"),
"{req_str}\n{body}\nexpected to not contain: error"
);
} else if stem.starts_with("error_") {
let rest = stem.strip_prefix("error_").unwrap();
let expected_str = rest.replace('_', " ").to_lowercase();
assert!(
lowercase_body.contains(&expected_str),
"{req_str}\n{lowercase_body}\nexpected to contain: {expected_str}"
);
assert!(
lowercase_body.contains("error"),
"{req_str}\n{body}\nexpected to contain: error"
);
}
}
echo_server.stop(true).await
}
#[actix_web::test]
async fn test_overwrite_variable() -> actix_web::Result<()> {
let req = get_request_to("/tests/sql_test_files/it_works_set_variable.sql")
.await?
.set_form(HashMap::<&str, &str>::from_iter([(
"what_does_it_do",
"does not overwrite variables",
)]))
.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("It works !"),
"{body_str}\nexpected to contain: It works !"
);
Ok(())
}
#[actix_web::test]
async fn test_json_body() -> actix_web::Result<()> {
let req = get_request_to("/tests/json_data.sql")
.await?
.to_srv_request();
let resp = main_handler(req).await?;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/json"
);
let body = test::read_body(resp).await;
let body_json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(
body_json,
serde_json::json!([{"message": "It works!"}, {"cool": "cool"}])
);
Ok(())
}
#[actix_web::test]
async fn test_csv_body() -> actix_web::Result<()> {
let req = get_request_to("/tests/csv_data.sql")
.await?
.to_srv_request();
let resp = main_handler(req).await?;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/csv; charset=utf-8"
);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(
body_str,
"id;msg\n0;Hello World !\n1;\"Tu gères ';' et '\"\"' ?\"\n"
);
Ok(())
}
async fn test_file_upload(target: &str) -> actix_web::Result<()> {
let req = get_request_to(target)
.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_direct() -> actix_web::Result<()> {
test_file_upload("/tests/upload_file_test.sql").await
}
#[actix_web::test]
async fn test_file_upload_through_runsql() -> actix_web::Result<()> {
test_file_upload("/tests/upload_file_runsql_test.sql").await
}
// Diabled because of
#[actix_web::test]
async fn test_blank_file_upload_field() -> 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=\"\"\r\n\
Content-Type: application/octet-stream\r\n\
\r\n\
\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("No file uploaded"),
"{body_str}\nexpected to contain: No file uploaded"
);
Ok(())
}
#[actix_web::test]
async fn test_file_upload_too_large() -> actix_web::Result<()> {
// Files larger than 123456 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(123457).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_large_form_field_roundtrip() -> actix_web::Result<()> {
// POST payloads smaller than 123456 bytes should be accepted
let long_string = "a".repeat(123454);
let req = get_request_to("/tests/display_form_field.sql")
.await?
.insert_header(("content-type", "application/x-www-form-urlencoded"))
.set_payload(["x=", &long_string].concat()) // total size is 123454 + 2 = 123456 bytes
.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("error"),
"{body_str}\nshouldn't have errors"
);
assert!(
body_str.contains(&long_string),
"{body_str}\nexpected to contain long string submitted"
);
Ok(())
}
#[actix_web::test]
async fn test_upload_file_data_url() -> actix_web::Result<()> {
let req = get_request_to("/tests/upload_file_data_url_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: image/svg+xml\r\n\
\r\n\
<svg></svg>\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();
// The file name suffix was ".txt", but the content type was "application/json"
// so the file should be treated as a JSON file
assert_eq!(body_str, "data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=");
Ok(())
}
#[actix_web::test]
async fn test_uploaded_file_name() -> actix_web::Result<()> {
let req = get_request_to("/tests/uploaded_file_name_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\
Some plain text.\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_eq!(body_str, "testfile.txt");
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]
async fn test_transaction_error() -> actix_web::Result<()> {
// First, request the page without any parameter. It should fail because
// of the not null constraint.
// But then, when we request again with a parameter, we should not see any side
// effect coming from the first transaction, and it should succeed
let data = make_app_data().await;
let path = match data.db.to_string().to_lowercase().as_str() {
"mysql" => "/tests/failed_transaction_mysql.sql",
"mssql" => "/tests/failed_transaction_mssql.sql",
_ => "/tests/failed_transaction.sql",
};
let req = get_request_to_with_data(path, data.clone())
.await?
.to_srv_request();
let resp = main_handler(req).await?;
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec())
.unwrap()
.to_ascii_lowercase();
assert!(
body_str.contains("error") && body_str.contains("null"),
"{body_str}\nexpected to contain: constraint failed"
);
// Now query again, with ?x=1447
let path_with_param = path.to_string() + "?x=1447";
let req = get_request_to_with_data(&path_with_param, data.clone())
.await?
.to_srv_request();
let resp = main_handler(req).await?;
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("1447"),
"{body_str}\nexpected to contain: 1447"
);
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, but received success: {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]
/// https://github.com/sqlpage/SQLPage/issues/738
async fn test_json_columns() {
let app_data = make_app_data().await;
if !matches!(
app_data.db.to_string().to_lowercase().as_str(),
"postgres" | "sqlite"
) {
log::info!("Skipping test_json_columns on database {}", app_data.db);
return;
}
let resp_result = req_path("/tests/json_columns.sql").await;
let resp = resp_result.expect("Failed to request /tests/json_columns.sql");
assert_eq!(resp.status(), http::StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
let body_html_escaped = body_str.replace("&quot;", "\"");
assert!(
!body_html_escaped.contains("error"),
"the request should not have failed, in: {body_html_escaped}"
);
assert!(body_html_escaped.contains("1GB Database"));
assert!(body_html_escaped.contains("Priority Support"));
assert!(
!body_html_escaped.contains("\"description\""),
"the json should have been parsed, not returned as a string, in: {body_html_escaped}"
);
assert!(
!body_html_escaped.contains("{"),
"the json should have been parsed, not returned as a string, in: {body_html_escaped}"
);
}
#[actix_web::test]
async fn test_static_files() {
let resp = req_path("/tests/it_works.txt").await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let body = test::read_body(resp).await;
assert_eq!(&body, &b"It works !"[..]);
}
#[actix_web::test]
async fn test_spaces_in_file_names() {
let resp = req_path("/tests/spaces%20in%20file%20name.sql")
.await
.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(body_str.contains("It works !"), "{body_str}");
}
#[actix_web::test]
async fn test_with_site_prefix() {
let mut config = test_config();
config.site_prefix = "/xxx/".to_string();
let state = AppState::init(&config).await.unwrap();
let app_data = actix_web::web::Data::new(state);
let resp = req_path_with_app_data("/xxx/tests/sql_test_files/it_works_simple.sql", app_data)
.await
.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("It works !"),
"{body_str}\nexpected to contain: It works !"
);
assert!(
body_str.contains("href=\"/xxx/"),
"{body_str}\nexpected to contain stylesheet link with site prefix"
);
}
async fn make_app_data_for_official_website() -> actix_web::web::Data<AppState> {
init_log();
let config_path = std::path::Path::new("examples/official-site/sqlpage");
let mut app_config = sqlpage::app_config::load_from_directory(config_path).unwrap();
app_config.web_root = PathBuf::from("examples/official-site");
app_config.database_url = "sqlite::memory:".to_string(); // the official site supports only sqlite. Ignore the DATABASE_URL env var
let app_state = make_app_data_from_config(app_config.clone()).await;
webserver::database::migrations::apply(&app_config, &app_state.db)
.await
.unwrap();
app_state
}
#[actix_web::test]
async fn test_official_website_documentation() {
let app_data = make_app_data_for_official_website().await;
let resp = req_path_with_app_data("/component.sql?component=button", app_data)
.await
.unwrap_or_else(|e| {
panic!("Failed to get response for /component.sql?component=button: {e}")
});
assert_eq!(resp.status(), http::StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains(r#"<button type="submit" form="poem" formaction="?action"#),
"{body_str}\nexpected to contain a button with formaction"
);
}
#[actix_web::test]
async fn test_official_website_basic_auth_example() {
let resp = req_path_with_app_data(
"/examples/authentication/basic_auth.sql",
make_app_data_for_official_website().await,
)
.await
.unwrap();
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("not authorized"),
"{body_str}\nexpected to contain Unauthorized"
);
}
/// https://github.com/sqlpage/SQLPage/issues/788
#[actix_web::test]
async fn test_failed_copy_followed_by_query() -> actix_web::Result<()> {
let app_data = make_app_data().await;
let big_csv = "col1,col2\nval1,val2\n".repeat(1000);
let req = get_request_to_with_data(
"/tests/sql_test_files/error_failed_to_import_the_csv.sql",
app_data.clone(),
)
.await?
.insert_header(("content-type", "multipart/form-data; boundary=1234567890"))
.set_payload(format!(
"--1234567890\r\n\
Content-Disposition: form-data; name=\"recon_csv_file_input\"; filename=\"data.csv\"\r\n\
Content-Type: text/csv\r\n\
\r\n\
{big_csv}\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("error"),
"{body_str}\nexpected to contain error message"
);
// On postgres, the error message should contain "The postgres COPY FROM STDIN command failed"
if matches!(app_data.db.to_string().to_lowercase().as_str(), "postgres") {
assert!(
body_str.contains("The postgres COPY FROM STDIN command failed"),
"{body_str}\nexpected to contain: The postgres COPY FROM STDIN command failed"
);
}
// Now make other requests to verify the connection is still usable
for path in [
"/tests/sql_test_files/it_works_lower.sql",
"/tests/sql_test_files/it_works_simple.sql",
"/tests/sql_test_files/it_works_path.sql",
] {
let req = get_request_to_with_data(path, app_data.clone())
.await?
.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("It works !"),
"{body_str}\nexpected to contain: It works !"
);
}
Ok(())
}
#[actix_web::test]
async fn test_routing_with_db_fs() {
let mut config = test_config();
// ignore this test if we are running on a temporary in-memory database
if config.database_url.contains("memory") {
return;
}
config.site_prefix = "/prefix/".to_string();
let state = AppState::init(&config).await.unwrap();
// Set up database filesystem
let create_table_sql = DbFsQueries::get_create_table_sql(state.db.connection.any_kind());
state
.db
.connection
.execute(format!("DROP TABLE IF EXISTS sqlpage_files; {create_table_sql}").as_ref())
.await
.unwrap();
// Insert test file into database using database-specific syntax
let insert_sql = match state.db.connection.any_kind() {
sqlx::any::AnyKind::Mssql => "INSERT INTO sqlpage_files(path, contents) VALUES ('on_db.sql', CONVERT(VARBINARY(MAX), 'select ''text'' as component, ''Hi from db !'' AS contents;'))",
_ => "INSERT INTO sqlpage_files(path, contents) VALUES ('on_db.sql', 'select ''text'' as component, ''Hi from db !'' AS contents;')"
};
state.db.connection.execute(insert_sql).await.unwrap();
let state = AppState::init(&config).await.unwrap();
let app_data = actix_web::web::Data::new(state);
// Test on_db.sql
let resp = req_path_with_app_data("/prefix/on_db.sql", app_data.clone())
.await
.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("Hi from db !"),
"{body_str}\nexpected to contain: Hi from db !"
);
}
#[actix_web::test]
async fn test_routing_with_prefix() {
let mut config = test_config();
config.site_prefix = "/prefix/".to_string();
let state = AppState::init(&config).await.unwrap();
let app_data = actix_web::web::Data::new(state);
// Test basic routing with prefix
let resp = req_path_with_app_data(
"/prefix/tests/sql_test_files/it_works_simple.sql",
app_data.clone(),
)
.await
.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("It works !"),
"{body_str}\nexpected to contain: It works !"
);
assert!(
body_str.contains("href=\"/prefix/"),
"{body_str}\nexpected to contain links with site prefix"
);
// Test 404 handling with prefix
let resp = req_path_with_app_data("/prefix/nonexistent.sql", app_data.clone())
.await
.expect_err("Expected 404 error")
.to_string();
assert!(
resp.contains("404"),
"Response should contain \"404\", but got:\n{resp}"
);
// Test forbidden paths with prefix
let resp = req_path_with_app_data("/prefix/sqlpage/migrations/0001_init.sql", app_data.clone())
.await
.expect_err("Expected forbidden error")
.to_string();
assert!(resp.to_lowercase().contains("forbidden"), "{resp}");
// accessing without prefix should redirect to the prefix
let resp = req_path_with_app_data("/tests/sql_test_files/it_works_simple.sql", app_data)
.await
.unwrap();
assert_eq!(resp.status(), http::StatusCode::MOVED_PERMANENTLY);
let location = resp
.headers()
.get("location")
.expect("location header should be present");
assert_eq!(location.to_str().unwrap(), "/prefix/");
}
#[actix_web::test]
async fn test_request_body() -> actix_web::Result<()> {
let req = get_request_to("/tests/request_body_test.sql")
.await?
.insert_header(("content-type", "text/plain"))
.set_payload("Hello, world!")
.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!"
);
// Test with form data - should return NULL
let req = get_request_to("/tests/request_body_test.sql")
.await?
.insert_header(("content-type", "application/x-www-form-urlencoded"))
.set_payload("key=value")
.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("NULL"),
"{body_str}\nexpected NULL for form data"
);
Ok(())
}
#[actix_web::test]
async fn test_request_body_base64() -> actix_web::Result<()> {
let binary_data = (0u8..=255u8).collect::<Vec<_>>();
let expected_base64 =
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &binary_data);
let req = get_request_to("/tests/request_body_base64_test.sql")
.await?
.insert_header(("content-type", "application/octet-stream"))
.set_payload(binary_data)
.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(&expected_base64),
"{body_str}\nexpected to contain base64: {expected_base64}"
);
// Test with form data - should return NULL
let req = get_request_to("/tests/request_body_base64_test.sql")
.await?
.insert_header(("content-type", "application/x-www-form-urlencoded"))
.set_payload("key=value")
.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("NULL"),
"{body_str}\nexpected NULL for form data"
);
Ok(())
}
#[actix_web::test]
async fn test_hidden_files() {
let resp_result = req_path("/tests/.hidden.sql").await;
assert!(
resp_result.is_err(),
"Accessing a hidden file should be forbidden, but received success: {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"),
);
}
async fn get_request_to_with_data(
path: &str,
data: actix_web::web::Data<AppState>,
) -> actix_web::Result<TestRequest> {
Ok(test::TestRequest::get()
.uri(path)
.insert_header(ContentType::plaintext())
.app_data(payload_config(&data))
.app_data(form_config(&data))
.app_data(data))
}
async fn get_request_to(path: &str) -> actix_web::Result<TestRequest> {
let data = make_app_data().await;
get_request_to_with_data(path, data).await
}
async fn make_app_data_from_config(config: AppConfig) -> actix_web::web::Data<AppState> {
let state = AppState::init(&config).await.unwrap();
actix_web::web::Data::new(state)
}
async fn make_app_data() -> actix_web::web::Data<AppState> {
init_log();
let config = test_config();
make_app_data_from_config(config).await
}
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?.to_srv_request();
main_handler(req).await
}
async fn srv_req_path_with_app_data(
path: impl AsRef<str>,
app_data: actix_web::web::Data<AppState>,
) -> actix_web::dev::ServiceRequest {
test::TestRequest::get()
.uri(path.as_ref())
.app_data(app_data)
.insert_header(("cookie", "test_cook=123"))
.insert_header(("authorization", "Basic dGVzdDp0ZXN0")) // test:test
.to_srv_request()
}
const REQ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(8);
async fn req_path_with_app_data(
path: impl AsRef<str>,
app_data: actix_web::web::Data<AppState>,
) -> anyhow::Result<actix_web::dev::ServiceResponse> {
let path = path.as_ref();
let req = srv_req_path_with_app_data(path, app_data).await;
let resp = tokio::time::timeout(REQ_TIMEOUT, main_handler(req))
.await
.map_err(|e| anyhow::anyhow!("Request to {path} timed out: {e}"))?
.map_err(|e| {
anyhow::anyhow!(
"Request to {path} failed with status {}: {e:#}",
e.as_response_error().status_code()
)
})?;
Ok(resp)
}
pub fn test_config() -> AppConfig {
let db_url = test_database_url();
serde_json::from_str::<AppConfig>(&format!(
r#"{{
"database_url": "{db_url}",
"max_database_pool_connections": 1,
"database_connection_retries": 3,
"database_connection_acquire_timeout_seconds": 15,
"allow_exec": true,
"max_uploaded_file_size": 123456,
"listen_on": "111.111.111.111:1",
"system_root_ca_certificates" : false
}}"#
))
.unwrap()
}
fn init_log() {
let _ = env_logger::builder()
.parse_default_env()
.is_test(true)
.try_init();
}
+19
View File
@@ -0,0 +1,19 @@
mod basic;
mod common;
mod components;
mod core;
mod data_formats;
mod errors;
mod requests;
mod transactions;
mod uploads;
pub use basic::*;
pub use common::*;
pub use components::*;
pub use core::*;
pub use data_formats::*;
pub use errors::*;
pub use requests::*;
pub use transactions::*;
pub use uploads::*;
+104
View File
@@ -0,0 +1,104 @@
use actix_web::{
http::StatusCode,
test,
};
use sqlpage::webserver::http::main_handler;
use crate::common::get_request_to;
#[actix_web::test]
async fn test_request_body() -> actix_web::Result<()> {
let req = get_request_to("/tests/requests/request_body_test.sql")
.await?
.insert_header(("content-type", "text/plain"))
.set_payload("Hello, world!")
.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!"
);
// Test with form data - should return NULL
let req = get_request_to("/tests/requests/request_body_test.sql")
.await?
.insert_header(("content-type", "application/x-www-form-urlencoded"))
.set_payload("key=value")
.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("NULL"),
"{body_str}\nexpected NULL for form data"
);
Ok(())
}
#[actix_web::test]
async fn test_request_body_base64() -> actix_web::Result<()> {
let binary_data = (0u8..=255u8).collect::<Vec<_>>();
let expected_base64 =
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &binary_data);
let req = get_request_to("/tests/requests/request_body_base64_test.sql")
.await?
.insert_header(("content-type", "application/octet-stream"))
.set_payload(binary_data)
.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(&expected_base64),
"{body_str}\nexpected to contain base64: {expected_base64}"
);
// Test with form data - should return NULL
let req = get_request_to("/tests/requests/request_body_base64_test.sql")
.await?
.insert_header(("content-type", "application/x-www-form-urlencoded"))
.set_payload("key=value")
.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("NULL"),
"{body_str}\nexpected NULL for form data"
);
Ok(())
}
#[actix_web::test]
async fn test_large_form_field_roundtrip() -> actix_web::Result<()> {
let long_string = "a".repeat(123454);
let req = get_request_to("/tests/components/display_form_field.sql")
.await?
.insert_header(("content-type", "application/x-www-form-urlencoded"))
.set_payload(["x=", &long_string].concat()) // total size is 123454 + 2 = 123456 bytes
.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("error"),
"{body_str}\nshouldn't have errors"
);
assert!(
body_str.contains(&long_string),
"{body_str}\nexpected to contain long string submitted"
);
Ok(())
}
@@ -1,7 +1,7 @@
select
'dynamic' as component,
sqlpage.run_sql (
'tests/display_text.sql',
'tests/components/display_text.sql',
CONCAT ('{"html":"', html, '"}') -- UNSAFE. Don't do that with untrusted data. We do it here because we can't use json_object (the syntax is not the same in all supported databases)
) as properties
from
@@ -1,4 +1,4 @@
set html = 'It ';
select 'dynamic' as component, sqlpage.run_sql('tests/display_text.sql') as properties;
select 'dynamic' as component, sqlpage.run_sql('tests/components/display_text.sql') as properties;
set html = 'works !';
select 'dynamic' as component, sqlpage.run_sql('tests/display_text.sql') as properties;
select 'dynamic' as component, sqlpage.run_sql('tests/components/display_text.sql') as properties;
@@ -1,4 +1,4 @@
-- Doesnt work on mssql because it does not support "create temporary table"
create temporary table temp_t(x text);
insert into temp_t(x) values ('It works !');
select 'dynamic' as component, sqlpage.run_sql('tests/select_temp_t.sql') AS properties;
select 'dynamic' as component, sqlpage.run_sql('tests/sql_test_files/select_temp_t.sql') AS properties;
+2
View File
@@ -0,0 +1,2 @@
-- see tests/sql_test_files/it_works_temp_table_accessible_in_run_sql.sql
select 'text' as component, x as contents from temp_t;
+100
View File
@@ -0,0 +1,100 @@
use actix_web::{
http::StatusCode,
test,
};
use sqlpage::webserver::http::main_handler;
use crate::common::{get_request_to_with_data, make_app_data};
#[actix_web::test]
async fn test_transaction_error() -> actix_web::Result<()> {
let data = make_app_data().await;
let path = match data.db.to_string().to_lowercase().as_str() {
"mysql" => "/tests/transactions/failed_transaction_mysql.sql",
"mssql" => "/tests/transactions/failed_transaction_mssql.sql",
_ => "/tests/transactions/failed_transaction.sql",
};
let req = get_request_to_with_data(path, data.clone())
.await?
.to_srv_request();
let resp = main_handler(req).await?;
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec())
.unwrap()
.to_ascii_lowercase();
assert!(
body_str.contains("error") && body_str.contains("null"),
"{body_str}\nexpected to contain: constraint failed"
);
// Now query again, with ?x=1447
let path_with_param = path.to_string() + "?x=1447";
let req = get_request_to_with_data(&path_with_param, data.clone())
.await?
.to_srv_request();
let resp = main_handler(req).await?;
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("1447"),
"{body_str}\nexpected to contain: 1447"
);
Ok(())
}
#[actix_web::test]
async fn test_failed_copy_followed_by_query() -> actix_web::Result<()> {
let app_data = make_app_data().await;
let big_csv = "col1,col2\nval1,val2\n".repeat(1000);
let req = get_request_to_with_data(
"/tests/sql_test_files/error_failed_to_import_the_csv.sql",
app_data.clone(),
)
.await?
.insert_header(("content-type", "multipart/form-data; boundary=1234567890"))
.set_payload(format!(
"--1234567890\r\n\
Content-Disposition: form-data; name=\"recon_csv_file_input\"; filename=\"data.csv\"\r\n\
Content-Type: text/csv\r\n\
\r\n\
{big_csv}\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("error"),
"{body_str}\nexpected to contain error message"
);
// On postgres, the error message should contain "The postgres COPY FROM STDIN command failed"
if matches!(app_data.db.to_string().to_lowercase().as_str(), "postgres") {
assert!(
body_str.contains("The postgres COPY FROM STDIN command failed"),
"{body_str}\nexpected to contain: The postgres COPY FROM STDIN command failed"
);
}
// Now make other requests to verify the connection is still usable
for path in [
"/tests/sql_test_files/it_works_lower.sql",
"/tests/sql_test_files/it_works_simple.sql",
"/tests/sql_test_files/it_works_path.sql",
] {
let req = get_request_to_with_data(path, app_data.clone())
.await?
.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("It works !"),
"{body_str}\nexpected to contain: It works !"
);
}
Ok(())
}
-1
View File
@@ -1 +0,0 @@
select 'dynamic' as component, sqlpage.run_sql('tests/upload_file_test.sql') as properties;
+168
View File
@@ -0,0 +1,168 @@
use actix_web::{
http::StatusCode,
test,
};
use sqlpage::webserver::http::main_handler;
use crate::common::get_request_to;
async fn test_file_upload(target: &str) -> actix_web::Result<()> {
let req = get_request_to(target)
.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_direct() -> actix_web::Result<()> {
test_file_upload("/tests/uploads/upload_file_test.sql").await
}
#[actix_web::test]
async fn test_file_upload_through_runsql() -> actix_web::Result<()> {
test_file_upload("/tests/uploads/upload_file_runsql_test.sql").await
}
#[actix_web::test]
async fn test_blank_file_upload_field() -> actix_web::Result<()> {
let req = get_request_to("/tests/uploads/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=\"\"\r\n\
Content-Type: application/octet-stream\r\n\
\r\n\
\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("No file uploaded"),
"{body_str}\nexpected to contain: No file uploaded"
);
Ok(())
}
#[actix_web::test]
async fn test_file_upload_too_large() -> actix_web::Result<()> {
let req = get_request_to("/tests/uploads/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(123457).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_upload_file_data_url() -> actix_web::Result<()> {
let req = get_request_to("/tests/uploads/upload_file_data_url_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: image/svg+xml\r\n\
\r\n\
<svg></svg>\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_eq!(body_str, "data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=");
Ok(())
}
#[actix_web::test]
async fn test_uploaded_file_name() -> actix_web::Result<()> {
let req = get_request_to("/tests/uploads/uploaded_file_name_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\
Some plain text.\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_eq!(body_str, "testfile.txt");
Ok(())
}
#[actix_web::test]
async fn test_csv_upload() -> actix_web::Result<()> {
let req = get_request_to("/tests/uploads/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(())
}
@@ -0,0 +1 @@
select 'dynamic' as component, sqlpage.run_sql('tests/uploads/upload_file_test.sql') as properties;