Files
2026-06-05 23:22:15 +02:00

318 lines
10 KiB
Rust

use actix_web::{http::StatusCode, test};
use sqlpage::{
AppState,
webserver::database::{DatabasePool, execute_queries::DbConnection},
webserver::{self, make_placeholder},
};
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();
if matches!(
state.db.info.database_type,
sqlpage::webserver::database::SupportedDatabase::Oracle
) {
return;
}
let drop_sql = "DROP TABLE IF EXISTS sqlpage_files";
state.db.connection.execute(drop_sql).await.unwrap();
let create_table_sql =
sqlpage::filesystem::DbFsQueries::get_create_table_sql(state.db.info.database_type);
state.db.connection.execute(create_table_sql).await.unwrap();
let insert_sql = format!(
"INSERT INTO sqlpage_files(path, contents) VALUES ('on_db.sql', {})",
make_placeholder(state.db.info.kind, 1)
);
insert_db_file_contents(
&state.db.connection,
&insert_sql,
"select ''text'' as component, ''Hi from db !'' AS contents;".as_bytes(),
)
.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 !"
);
}
#[cfg(unix)]
#[actix_web::test]
async fn test_non_unicode_static_path_returns_bad_request_with_db_fs() {
let mut config = test_config();
if !config.database_url.starts_with("sqlite") {
return;
}
config.database_url =
"sqlite://file:test_non_unicode_static_path?mode=memory&cache=shared".to_string();
let state = AppState::init(&config).await.unwrap();
let expected_db_path = "\u{FFFD}.txt";
let mut conn = state.db.connection.acquire().await.unwrap();
if let DbConnection::Sqlite(conn) = &mut conn {
(&mut **conn)
.execute(sqlpage::filesystem::DbFsQueries::get_create_table_sql(
sqlpage::webserver::database::SupportedDatabase::Sqlite,
))
.await
.unwrap();
} else {
unreachable!("test uses sqlite");
}
let insert_sql = format!(
"INSERT INTO sqlpage_files(path, contents) VALUES ({}, {})",
make_placeholder(state.db.info.kind, 1),
make_placeholder(state.db.info.kind, 2)
);
insert_db_file_path_contents(
&mut conn,
&insert_sql,
expected_db_path,
"file from db fs".as_bytes(),
)
.await
.unwrap();
drop(conn);
let state = AppState::init(&config).await.unwrap();
let app_data = actix_web::web::Data::new(state);
let req = test::TestRequest::get()
.uri("/%FF.txt")
.app_data(app_data)
.to_srv_request();
let err = sqlpage::webserver::http::main_handler(req)
.await
.expect_err("non-unicode path should not panic and must return bad request");
assert_eq!(
err.as_response_error().status_code(),
StatusCode::BAD_REQUEST
);
}
async fn insert_db_file_contents(
pool: &DatabasePool,
sql: &str,
contents: &[u8],
) -> sqlx::Result<()> {
match pool {
DatabasePool::Sqlite(pool) => sqlx::query::<sqlx::Sqlite>(sqlx::AssertSqlSafe(sql))
.bind(contents)
.execute(pool)
.await
.map(|_| ()),
DatabasePool::Postgres(pool) => sqlx::query::<sqlx::Postgres>(sqlx::AssertSqlSafe(sql))
.bind(contents)
.execute(pool)
.await
.map(|_| ()),
DatabasePool::MySql(pool) => sqlx::query::<sqlx::MySql>(sqlx::AssertSqlSafe(sql))
.bind(contents)
.execute(pool)
.await
.map(|_| ()),
DatabasePool::Mssql(pool) => sqlx::query::<sqlx_sqlserver::Mssql>(sqlx::AssertSqlSafe(sql))
.bind(contents)
.execute(pool)
.await
.map(|_| ()),
DatabasePool::Odbc(pool) => sqlx::query::<sqlx_odbc::Odbc>(sqlx::AssertSqlSafe(sql))
.bind(contents)
.execute(pool)
.await
.map(|_| ()),
}
}
async fn insert_db_file_path_contents(
conn: &mut DbConnection,
sql: &str,
path: &str,
contents: &[u8],
) -> sqlx::Result<()> {
match conn {
DbConnection::Sqlite(conn) => sqlx::query::<sqlx::Sqlite>(sqlx::AssertSqlSafe(sql))
.bind(path)
.bind(contents)
.execute(&mut **conn)
.await
.map(|_| ()),
_ => unreachable!("test uses sqlite"),
}
}
#[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/component_rendering/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("should handle 404");
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("404"),
"Response should contain \"404\", but got:\n{body_str}"
);
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/component_rendering/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("Unauthorized"),
"{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
}