173 lines
6.2 KiB
Rust
173 lines
6.2 KiB
Rust
use chusql_driver::{Client, Error, Options};
|
|||
|
|
use serde_json::{Value, json};
|
||
|
|
use std::time::Duration;
|
||
|
|
use tokio::{
|
||
|
|
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||
|
|
net::TcpListener,
|
||
|
|
};
|
||
|
|
|
||
|
|
async fn server(script: Vec<(Value, &'static str)>) -> (String, tokio::task::JoinHandle<()>) {
|
||
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||
|
|
let address = listener.local_addr().unwrap().to_string();
|
||
|
|
let task = tokio::spawn(async move {
|
||
|
|
let (stream, _) = listener.accept().await.unwrap();
|
||
|
|
let mut stream = BufReader::new(stream);
|
||
|
|
for (expected, response) in script {
|
||
|
|
let mut line = String::new();
|
||
|
|
stream.read_line(&mut line).await.unwrap();
|
||
|
|
assert_eq!(serde_json::from_str::<Value>(&line).unwrap(), expected);
|
||
|
|
// Split writes also exercise TCP framing across packet boundaries.
|
||
|
|
for chunk in response.as_bytes().chunks(7) {
|
||
|
|
stream.get_mut().write_all(chunk).await.unwrap();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
(address, task)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn handshake() -> Vec<(Value, &'static str)> {
|
||
|
|
vec![
|
||
|
|
(
|
||
|
|
json!({"method":"hello","protocol":1}),
|
||
|
|
"{\"status\":\"hello\",\"protocol\":1,\"server\":\"chusql-server\"}\n",
|
||
|
|
),
|
||
|
|
(
|
||
|
|
json!({"method":"login","user":"app","password":"秘密\npass"}),
|
||
|
|
"{\"status\":\"ok\",\"user\":\"app\",\"admin\":false}\n",
|
||
|
|
),
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn authenticated_session_preserves_results_and_recovers_from_sql_errors() {
|
||
|
|
let mut script = handshake();
|
||
|
|
script.extend([
|
||
|
|
(json!({"method":"query","sql":"SELECT * FROM users"}), "{\"status\":\"result\",\"columns\":[\"id\",\"name\",\"active\",\"extra\"],\"rows\":[[9223372036854775808,\"小楚\",true,null]],\"rowCount\":1,\"truncated\":true,\"database\":\"app\"}\n"),
|
||
|
|
(json!({"method":"query","sql":"bad SQL"}), "{\"status\":\"error\",\"code\":\"syntax_error\",\"message\":\"invalid SQL\"}\n"),
|
||
|
|
(json!({"method":"ping"}), "{\"status\":\"pong\"}\n"),
|
||
|
|
]);
|
||
|
|
let (address, task) = server(script).await;
|
||
|
|
let client = Client::connect(&address, "app", "秘密\npass")
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
let result = client.query("SELECT * FROM users").await.unwrap();
|
||
|
|
assert_eq!(
|
||
|
|
result.rows[0],
|
||
|
|
vec![
|
||
|
|
json!(9223372036854775808_u64),
|
||
|
|
json!("小楚"),
|
||
|
|
json!(true),
|
||
|
|
Value::Null
|
||
|
|
]
|
||
|
|
);
|
||
|
|
assert!(result.truncated);
|
||
|
|
assert_eq!(result.row_count, 1);
|
||
|
|
assert_eq!(result.database.as_deref(), Some("app"));
|
||
|
|
assert!(
|
||
|
|
matches!(client.query("bad SQL").await, Err(Error::Server { code, .. }) if code == "syntax_error")
|
||
|
|
);
|
||
|
|
client.clone().ping().await.unwrap();
|
||
|
|
client.close().await;
|
||
|
|
assert!(matches!(client.ping().await, Err(Error::Closed)));
|
||
|
|
task.await.unwrap();
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn rejects_wrong_protocol_and_failed_login() {
|
||
|
|
let (address, task) = server(vec![(
|
||
|
|
json!({"method":"hello","protocol":1}),
|
||
|
|
"{\"status\":\"hello\",\"protocol\":2}\n",
|
||
|
|
)])
|
||
|
|
.await;
|
||
|
|
assert!(matches!(
|
||
|
|
Client::connect(address, "app", "pw").await,
|
||
|
|
Err(Error::Protocol(_))
|
||
|
|
));
|
||
|
|
task.await.unwrap();
|
||
|
|
let mut script = handshake();
|
||
|
|
script[1].1 =
|
||
|
|
"{\"status\":\"error\",\"code\":\"unauthorized\",\"message\":\"invalid credentials\"}\n";
|
||
|
|
let (address, task) = server(script).await;
|
||
|
|
assert!(
|
||
|
|
matches!(Client::connect(address, "app", "秘密\npass").await, Err(Error::Server { code, .. }) if code == "unauthorized")
|
||
|
|
);
|
||
|
|
task.await.unwrap();
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn malformed_oversized_and_unterminated_responses_discard_connection() {
|
||
|
|
for response in [
|
||
|
|
"not JSON\n",
|
||
|
|
"{\"status\":\"pong\"}",
|
||
|
|
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n",
|
||
|
|
] {
|
||
|
|
let mut script = handshake();
|
||
|
|
script.push((json!({"method":"ping"}), response));
|
||
|
|
let (address, task) = server(script).await;
|
||
|
|
let client = Client::connect_with_options(
|
||
|
|
address,
|
||
|
|
"app",
|
||
|
|
"秘密\npass",
|
||
|
|
Options {
|
||
|
|
max_response_bytes: 128,
|
||
|
|
..Options::default()
|
||
|
|
},
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
assert!(client.ping().await.is_err());
|
||
|
|
assert!(matches!(client.ping().await, Err(Error::Closed)));
|
||
|
|
task.await.unwrap();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn timeout_and_cancellation_do_not_reuse_pending_responses() {
|
||
|
|
for cancel in [false, true] {
|
||
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||
|
|
let address = listener.local_addr().unwrap();
|
||
|
|
let (sent, received) = tokio::sync::oneshot::channel();
|
||
|
|
let task = tokio::spawn(async move {
|
||
|
|
let (stream, _) = listener.accept().await.unwrap();
|
||
|
|
let mut stream = BufReader::new(stream);
|
||
|
|
for (_, response) in handshake() {
|
||
|
|
let mut line = String::new();
|
||
|
|
stream.read_line(&mut line).await.unwrap();
|
||
|
|
stream
|
||
|
|
.get_mut()
|
||
|
|
.write_all(response.as_bytes())
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
}
|
||
|
|
let mut line = String::new();
|
||
|
|
stream.read_line(&mut line).await.unwrap();
|
||
|
|
sent.send(()).unwrap();
|
||
|
|
line.clear();
|
||
|
|
assert_eq!(stream.read_line(&mut line).await.unwrap(), 0);
|
||
|
|
});
|
||
|
|
let client = Client::connect_with_options(
|
||
|
|
address,
|
||
|
|
"app",
|
||
|
|
"秘密\npass",
|
||
|
|
Options {
|
||
|
|
timeout: Duration::from_millis(500),
|
||
|
|
..Options::default()
|
||
|
|
},
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
let pending = tokio::spawn({
|
||
|
|
let client = client.clone();
|
||
|
|
async move { client.ping().await }
|
||
|
|
});
|
||
|
|
received.await.unwrap();
|
||
|
|
if cancel {
|
||
|
|
pending.abort();
|
||
|
|
assert!(pending.await.unwrap_err().is_cancelled());
|
||
|
|
} else {
|
||
|
|
assert!(matches!(pending.await.unwrap(), Err(Error::Timeout)));
|
||
|
|
}
|
||
|
|
assert!(matches!(client.ping().await, Err(Error::Closed)));
|
||
|
|
task.await.unwrap();
|
||
|
|
}
|
||
|
|
}
|