use axum::body::Body;
use axum::extract::Request;
use axum::http::{header, StatusCode};
use larust_db::DbPlugin;
use tower::ServiceExt;
fn get(path: &str, cookie: Option<&str>) -> Request {
let mut builder = Request::get(path);
if let Some(cookie) = cookie {
builder = builder.header(header::COOKIE, cookie);
}
builder.body(Body::empty()).unwrap()
}
fn post_form(path: &str, cookie: Option<&str>, fields: &[(&str, &str)]) -> Request {
let body = form_urlencoded::Serializer::new(String::new())
.extend_pairs(fields)
.finish();
let mut builder =
Request::post(path).header(header::CONTENT_TYPE, "application/x-www-form-urlencoded");
if let Some(cookie) = cookie {
builder = builder.header(header::COOKIE, cookie);
}
builder.body(Body::from(body)).unwrap()
}
fn post_file(path: &str, cookie: Option<&str>, filename: &str, contents: &str) -> Request {
let boundary = "----larust-db-test-boundary";
let body = format!(
"--{boundary}\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"{filename}\"\r\n\
Content-Type: application/sql\r\n\r\n\
{contents}\r\n\
--{boundary}--\r\n"
);
let mut builder = Request::post(path).header(
header::CONTENT_TYPE,
format!("multipart/form-data; boundary={boundary}"),
);
if let Some(cookie) = cookie {
builder = builder.header(header::COOKIE, cookie);
}
builder.body(Body::from(body)).unwrap()
}
fn session_cookie(response: &axum::response::Response) -> String {
response
.headers()
.get(header::SET_COOKIE)
.expect("response should set a session cookie")
.to_str()
.unwrap()
.split(';')
.next()
.unwrap()
.to_string()
}
async fn body_string(response: axum::response::Response) -> String {
String::from_utf8(
axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap()
.to_vec(),
)
.unwrap()
}
#[tokio::test]
async fn db_dashboard_end_to_end() {
let dir = tempfile::tempdir().unwrap().keep();
let database_url = format!("sqlite://{}/test.sqlite", dir.display());
larust_orm::connect(&database_url).await.unwrap();
let pool = larust_orm::pool().unwrap().clone();
sqlx::query(
"CREATE TABLE widgets (id INTEGER PRIMARY KEY AUTOINCREMENT, \
name TEXT NOT NULL, notes TEXT, payload BLOB)",
)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO widgets (name, payload) VALUES ('Seed', X'DEADBEEF')")
.execute(&pool)
.await
.unwrap();
let router = larust_http::Router::new()
.plugin(DbPlugin)
.with_sessions(&pool, false)
.await
.unwrap()
.into_axum_router();
let db_dir = tempfile::tempdir().unwrap();
larust_db::connect(db_dir.path().join("test.redb"))
.await
.unwrap();
std::env::set_var("DB_DASHBOARD_PASSWORD", "s3cret");
let response = router.clone().oneshot(get("/xr-db", None)).await.unwrap();
assert!(response.status().is_redirection());
assert_eq!(
response
.headers()
.get(header::LOCATION)
.and_then(|v| v.to_str().ok()),
Some("/xr-db/login")
);
let response = router
.clone()
.oneshot(post_form("/xr-db/login", None, &[("password", "wrong")]))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_string(response).await;
assert!(body.contains("Incorrect password"));
let response = router
.clone()
.oneshot(post_form("/xr-db/login", None, &[("password", "s3cret")]))
.await
.unwrap();
assert!(response.status().is_redirection());
let cookie = session_cookie(&response);
let response = router
.clone()
.oneshot(get("/xr-db", Some(&cookie)))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_string(response).await;
assert!(body.contains("widgets"));
assert!(body.contains(r#"class="table-nav-item" href="/xr-db/t/widgets""#));
let response = router
.clone()
.oneshot(get("/xr-db/t/not_a_real_table", Some(&cookie)))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let response = router
.clone()
.oneshot(get("/xr-db/t/not_a_real_table/structure", Some(&cookie)))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let response = router
.clone()
.oneshot(get("/xr-db/t/widgets/structure", Some(&cookie)))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_string(response).await;
assert!(body.contains("Columns"));
assert!(body.contains("Indexes"));
assert!(body.contains("Foreign keys"));
assert!(body.contains("PK"));
let response = router
.clone()
.oneshot(post_form(
"/xr-db/t/widgets/insert",
Some(&cookie),
&[("name", "Sprocket"), ("notes", "")],
))
.await
.unwrap();
assert!(response.status().is_redirection());
let response = router
.clone()
.oneshot(get("/xr-db/t/widgets", Some(&cookie)))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_string(response).await;
assert!(body.contains("Sprocket"));
assert!(body.contains("null-value"));
assert!(body.contains("<blob, 4 bytes>"));
let response = router
.clone()
.oneshot(post_form(
"/xr-db/t/widgets/update",
Some(&cookie),
&[(r#"pk_id" OR "1"="1"#, "2"), ("name", "Injected")],
))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let response = router
.clone()
.oneshot(post_form(
"/xr-db/t/widgets/delete",
Some(&cookie),
&[(r#"pk_id" OR "1"="1"#, "2")],
))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let response = router
.clone()
.oneshot(get("/xr-db/t/widgets", Some(&cookie)))
.await
.unwrap();
let body = body_string(response).await;
assert!(body.contains("Sprocket"));
assert!(!body.contains("Injected"));
let response = router
.clone()
.oneshot(post_form(
"/xr-db/t/widgets/update",
Some(&cookie),
&[
("pk_id", "1"),
("name", "Seed"),
("payload", "should not stick"),
],
))
.await
.unwrap();
assert!(response.status().is_redirection());
let response = router
.clone()
.oneshot(get("/xr-db/t/widgets", Some(&cookie)))
.await
.unwrap();
let body = body_string(response).await;
assert!(
body.contains("<blob, 4 bytes>"),
"blob column should be unchanged, still 4 bytes: {body}"
);
let response = router
.clone()
.oneshot(get("/xr-db/t/widgets/edit?pk_id=2", Some(&cookie)))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_string(response).await;
assert!(body.contains("Sprocket"));
let response = router
.clone()
.oneshot(post_form(
"/xr-db/t/widgets/update",
Some(&cookie),
&[("pk_id", "2"), ("name", "Widget"), ("notes", "updated")],
))
.await
.unwrap();
assert!(response.status().is_redirection());
let response = router
.clone()
.oneshot(get("/xr-db/t/widgets", Some(&cookie)))
.await
.unwrap();
let body = body_string(response).await;
assert!(body.contains("Widget"));
assert!(body.contains("updated"));
let response = router
.clone()
.oneshot(post_form(
"/xr-db/sql",
Some(&cookie),
&[("sql", "SELECT COUNT(*) AS n FROM widgets")],
))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_string(response).await;
assert!(body.contains("1 row(s) returned"));
let response = router
.clone()
.oneshot(post_form(
"/xr-db/sql",
Some(&cookie),
&[("sql", "SELECT * FROM this_table_does_not_exist")],
))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_string(response).await;
assert!(body.contains("class=\"error\""));
let response = router
.clone()
.oneshot(post_form(
"/xr-db/t/widgets/delete",
Some(&cookie),
&[("pk_id", "2")],
))
.await
.unwrap();
assert!(response.status().is_redirection());
let response = router
.clone()
.oneshot(get("/xr-db/t/widgets", Some(&cookie)))
.await
.unwrap();
let body = body_string(response).await;
assert!(!body.contains("Widget"));
let response = router
.clone()
.oneshot(post_file(
"/xr-db/import",
Some(&cookie),
"seed.sql",
"CREATE TABLE imported_widgets (id INTEGER PRIMARY KEY, note TEXT);\n\
INSERT INTO imported_widgets (note) VALUES ('from import');",
))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_string(response).await;
assert!(body.contains("Import ran successfully"));
let response = router
.clone()
.oneshot(get("/xr-db/t/imported_widgets", Some(&cookie)))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_string(response).await;
assert!(body.contains("from import"));
let response = router
.clone()
.oneshot(post_file(
"/xr-db/import",
Some(&cookie),
"broken.sql",
"THIS IS NOT VALID SQL;",
))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_string(response).await;
assert!(body.contains("class=\"error\""));
let response = router
.clone()
.oneshot(post_form(
"/xr-db/kv/set",
Some(&cookie),
&[("key", "greeting"), ("value", "hello")],
))
.await
.unwrap();
assert!(response.status().is_redirection());
let response = router
.clone()
.oneshot(get("/xr-db/kv", Some(&cookie)))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_string(response).await;
assert!(body.contains("greeting"));
assert!(body.contains("hello"));
let response = router
.clone()
.oneshot(post_form("/xr-db/kv/greeting/delete", Some(&cookie), &[]))
.await
.unwrap();
assert!(response.status().is_redirection());
let response = router
.clone()
.oneshot(get("/xr-db/kv", Some(&cookie)))
.await
.unwrap();
let body = body_string(response).await;
assert!(!body.contains("greeting"));
let _ = router
.clone()
.oneshot(post_form("/xr-db/logout", Some(&cookie), &[]))
.await
.unwrap();
let response = router
.clone()
.oneshot(get("/xr-db", Some(&cookie)))
.await
.unwrap();
assert!(response.status().is_redirection());
}