d718c5a372
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled
258 lines
8.6 KiB
Rust
258 lines
8.6 KiB
Rust
use actix_web::{http::StatusCode, test};
|
|
use sqlpage::{
|
|
AppState,
|
|
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)
|
|
);
|
|
sqlx::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(
|
|
sqlpage::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(&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 = 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
|
|
);
|
|
}
|
|
|
|
#[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
|
|
}
|