285 lines
9.4 KiB
Rust
285 lines
9.4 KiB
Rust
use actix_web::{http::StatusCode, test};
|
||
use sqlpage::{
|
||
AppState,
|
||
webserver::{self, make_placeholder},
|
||
};
|
||
use sqlx::executor::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 {
|
||
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_datagrid_description_presence_controls_placeholder() {
|
||
let resp = req_path("/tests/components/datagrid_icon_only.sql")
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(resp.status(), StatusCode::OK);
|
||
let body = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap();
|
||
assert!(body.contains("Facebook"), "{body}");
|
||
assert!(body.contains("Empty"), "{body}");
|
||
assert!(body.contains("Missing"), "{body}");
|
||
assert!(body.contains("<svg"), "{body}");
|
||
assert_eq!(body.matches('–').count(), 1, "{body}");
|
||
}
|
||
|
||
#[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,
|
||
webserver::database::SupportedDatabase::Oracle
|
||
) {
|
||
return;
|
||
}
|
||
|
||
let create_table_sql =
|
||
sqlpage::filesystem::DbFsQueries::get_create_table_sql(state.db.info.database_type);
|
||
// Other tests share this database, so never drop a table their initialized state may use.
|
||
if state
|
||
.db
|
||
.connection
|
||
.execute("SELECT 1 FROM sqlpage_files WHERE 1 = 0")
|
||
.await
|
||
.is_err()
|
||
{
|
||
state.db.connection.execute(create_table_sql).await.unwrap();
|
||
}
|
||
state
|
||
.db
|
||
.connection
|
||
.execute("DELETE FROM sqlpage_files WHERE path = 'on_db.sql'")
|
||
.await
|
||
.unwrap();
|
||
let insert_sql = format!(
|
||
"INSERT INTO sqlpage_files(path, contents) VALUES ('on_db.sql', {})",
|
||
make_placeholder(state.db.info.kind, 1)
|
||
);
|
||
sqlx::query::query(&insert_sql)
|
||
.bind("select ''text'' as component, ''Hi from db !'' AS contents;".as_bytes())
|
||
.execute(&state.db.connection)
|
||
.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();
|
||
|
||
(&mut *conn)
|
||
.execute(sqlpage::filesystem::DbFsQueries::get_create_table_sql(
|
||
webserver::database::SupportedDatabase::Sqlite,
|
||
))
|
||
.await
|
||
.unwrap();
|
||
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)
|
||
);
|
||
sqlx::query::query(&insert_sql)
|
||
.bind(expected_db_path)
|
||
.bind("file from db fs".as_bytes())
|
||
.execute(&mut *conn)
|
||
.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 = 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
|
||
);
|
||
}
|
||
|
||
#[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 = 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
|
||
}
|