use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
use std::time::Duration;
use tricoredb::{params, Client, ErrorKind, Options};
const TAG_REQUEST: u8 = 2;
const TAG_RESPONSE: u8 = 3;
const TAG_ERROR: u8 = 6;
const TAG_HELLO_OK: u8 = 8;
const TAG_AUTH_OK: u8 = 9;
fn write_frame(stream: &mut TcpStream, tag: u8, body: &str) -> std::io::Result<()> {
let mut frame = vec![1u8, tag];
frame.extend_from_slice(&(body.len() as u32).to_be_bytes());
frame.extend_from_slice(body.as_bytes());
stream.write_all(&frame)?;
stream.flush()
}
fn read_frame(stream: &mut TcpStream) -> std::io::Result<(u8, Vec<u8>)> {
let mut header = [0u8; 6];
stream.read_exact(&mut header)?;
let length = u32::from_be_bytes([header[2], header[3], header[4], header[5]]) as usize;
let mut body = vec![0u8; length];
if length > 0 {
stream.read_exact(&mut body)?;
}
Ok((header[1], body))
}
struct Peer {
options: Options,
_thread: thread::JoinHandle<()>,
}
fn peer(script: impl FnOnce(&mut TcpStream) + Send + 'static) -> Peer {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let port = listener.local_addr().expect("addr").port();
let handle = thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
script(&mut stream);
}
});
Peer {
options: Options::new("127.0.0.1", port)
.user("admin")
.secret("pw")
.connect_timeout(Duration::from_secs(5)),
_thread: handle,
}
}
fn handshake(stream: &mut TcpStream) {
let _ = read_frame(stream); let _ = write_frame(
stream,
TAG_HELLO_OK,
r#"{"ok":true,"server_version":{"major":1,"minor":0},"message":"ok","features":7}"#,
);
let _ = read_frame(stream); let _ = write_frame(stream, TAG_AUTH_OK, r#"{"ok":true,"session_id":"s-1"}"#);
}
fn answer_one(stream: &mut TcpStream, response: &str) {
let _ = read_frame(stream);
let _ = write_frame(stream, TAG_RESPONSE, response);
let _ = read_frame(stream);
}
#[test]
fn a_not_leader_refusal_is_typed_and_carries_the_leader_address() {
let peer = peer(|stream| {
handshake(stream);
answer_one(
stream,
r#"{"request_id":"r1","status":"error","data":{"Message":"not the raft leader — send writes to `n2`"},"diagnostics":{"error_code":"not_leader","leader_hint":"10.9.9.7:8427"}}"#,
);
});
let mut db = Client::connect(&peer.options.clone().follow_leader(false)).expect("connect");
let error = db
.execute_params("INSERT INTO t VALUES (?)", ¶ms![1])
.expect_err("a follower refuses the write");
assert_eq!(error.kind, ErrorKind::Server);
assert_eq!(error.code(), Some("not_leader"));
assert!(
error.is_redirect(),
"the code, not the message, decides this"
);
assert_eq!(error.leader_hint(), Some("10.9.9.7:8427"));
assert!(!error.is_connection_fatal(), "the connection still works");
assert!(error.to_string().contains("10.9.9.7:8427"), "{error}");
}
#[test]
fn a_mid_election_refusal_has_a_code_but_no_address() {
let peer = peer(|stream| {
handshake(stream);
answer_one(
stream,
r#"{"request_id":"r1","status":"error","data":{"Message":"not the raft leader"},"diagnostics":{"error_code":"not_leader"}}"#,
);
});
let mut db = Client::connect(&peer.options.clone().follow_leader(false)).expect("connect");
let error = db.execute("INSERT INTO t VALUES (1)").expect_err("refused");
assert!(error.is_redirect());
assert_eq!(
error.leader_hint(),
None,
"an absent hint means the destination is unknown, not that there was no redirect"
);
}
#[test]
fn an_ordinary_failure_is_not_read_as_a_redirect() {
let peer = peer(|stream| {
handshake(stream);
answer_one(
stream,
r#"{"request_id":"r1","status":"error","data":{"Message":"syntax error"},"diagnostics":{"error_code":"request.invalid"}}"#,
);
});
let mut db = Client::connect(&peer.options).expect("connect");
let error = db.execute("NOT SQL").expect_err("refused");
assert!(!error.is_redirect());
assert_eq!(error.code(), Some("request.invalid"));
assert_eq!(error.message(), "syntax error");
}
#[test]
fn a_handshake_refusal_is_reported_as_one() {
let peer = peer(|stream| {
let _ = read_frame(stream);
let _ = write_frame(
stream,
TAG_HELLO_OK,
r#"{"ok":false,"message":"unsupported protocol version","code":"handshake.version"}"#,
);
let _ = read_frame(stream);
});
let error = Client::connect(&peer.options).expect_err("the handshake was refused");
assert_eq!(error.kind, ErrorKind::Handshake);
assert!(error.message().contains("unsupported protocol"), "{error}");
}
#[test]
fn an_auth_ok_frame_carrying_ok_false_is_still_a_refusal() {
let peer = peer(|stream| {
let _ = read_frame(stream);
let _ = write_frame(
stream,
TAG_HELLO_OK,
r#"{"ok":true,"message":"ok","features":7}"#,
);
let _ = read_frame(stream);
let _ = write_frame(
stream,
TAG_AUTH_OK,
r#"{"ok":false,"message":"bad password"}"#,
);
let _ = read_frame(stream);
});
let error = Client::connect(&peer.options).expect_err("authentication was refused");
assert_eq!(error.kind, ErrorKind::Auth);
assert_eq!(error.message(), "bad password");
}
#[test]
fn a_connection_level_error_frame_is_refused_with_its_code() {
let peer = peer(|stream| {
handshake(stream);
let _ = read_frame(stream);
let _ = write_frame(
stream,
TAG_ERROR,
r#"{"message":"frame too large","code":"frame_too_large"}"#,
);
let _ = read_frame(stream);
});
let mut db = Client::connect(&peer.options).expect("connect");
let error = db
.execute("SELECT 1")
.expect_err("the server refused the frame");
assert_eq!(error.kind, ErrorKind::Refused);
assert_eq!(error.code(), Some("frame_too_large"));
assert!(error.is_connection_fatal());
}
#[test]
fn a_declared_payload_above_the_ceiling_is_refused_before_it_is_read() {
let peer = peer(|stream| {
handshake(stream);
let _ = read_frame(stream);
let mut header = vec![1u8, TAG_AUTH_OK];
header.extend_from_slice(&((64 * 1024u32) + 1).to_be_bytes());
let _ = stream.write_all(&header);
let _ = stream.flush();
let _ = read_frame(stream);
});
let mut db = Client::connect(&peer.options).expect("connect");
let error = db.ping().expect_err("the frame is refused");
assert_eq!(error.kind, ErrorKind::Protocol, "{error}");
assert!(error.message().contains("limit"), "{error}");
assert!(db.is_poisoned(), "the stream can no longer be trusted");
}
#[test]
fn a_frame_version_this_client_cannot_read_is_refused() {
let peer = peer(|stream| {
handshake(stream);
let _ = read_frame(stream);
let _ = stream.write_all(&[2u8, TAG_RESPONSE, 0, 0, 0, 0]);
let _ = stream.flush();
let _ = read_frame(stream);
});
let mut db = Client::connect(&peer.options).expect("connect");
let error = db.ping().expect_err("a newer frame layout is refused");
assert_eq!(error.kind, ErrorKind::Protocol);
assert!(error.message().contains("version 2"), "{error}");
}
#[test]
fn a_peer_that_hangs_up_mid_frame_poisons_the_connection() {
let peer = peer(|stream| {
handshake(stream);
let _ = read_frame(stream);
let _ = stream.write_all(&[1u8, TAG_RESPONSE, 0, 0, 0, 10, b'{']);
let _ = stream.flush();
});
let mut db = Client::connect(&peer.options).expect("connect");
let error = db.execute("SELECT 1").expect_err("the peer went away");
assert!(
matches!(error.kind, ErrorKind::Io | ErrorKind::Protocol),
"{error}"
);
assert!(db.is_poisoned());
let again = db.ping().expect_err("a poisoned connection stays refused");
assert_eq!(again.kind, ErrorKind::Closed, "{again}");
}
#[test]
fn a_reply_that_never_arrives_ends_at_the_read_timeout() {
let peer = peer(|stream| {
handshake(stream);
let _ = read_frame(stream);
thread::sleep(Duration::from_secs(3));
});
let mut db = Client::connect(&peer.options).expect("connect");
db.set_read_timeout(Some(Duration::from_millis(150)))
.expect("set the read timeout");
let started = std::time::Instant::now();
let error = db.execute("SELECT 1").expect_err("no reply came");
assert_eq!(error.kind, ErrorKind::Timeout, "{error}");
assert!(
started.elapsed() < Duration::from_secs(2),
"it did not wait"
);
assert!(
db.is_poisoned(),
"the reply may still arrive, so the socket cannot be reused"
);
}
#[test]
fn a_status_this_client_does_not_know_is_treated_as_a_failure() {
let peer = peer(|stream| {
handshake(stream);
answer_one(
stream,
r#"{"request_id":"r1","status":"not_implemented","data":{"Message":"Cache::XGroup is refused in V1"}}"#,
);
});
let mut db = Client::connect(&peer.options).expect("connect");
let error = db
.request(serde_json::json!({"Cache": {"XGroup": {}}}))
.expect_err("anything but ok is a failure");
assert_eq!(error.kind, ErrorKind::Server);
assert!(error.message().contains("XGroup"), "{error}");
}
#[test]
fn warnings_reach_the_caller_on_a_successful_response() {
let peer = peer(|stream| {
handshake(stream);
answer_one(
stream,
r#"{"request_id":"r1","status":"ok","data":{"Message":"done"},"diagnostics":{"route":"local","elapsed_ms":4,"warnings":["shard 2 was unreachable"]}}"#,
);
});
let mut db = Client::connect(&peer.options).expect("connect");
let response = db.execute("CREATE TABLE t (id INT)").expect("ok");
assert_eq!(response.status, "ok");
assert_eq!(response.warnings, vec!["shard 2 was unreachable"]);
assert_eq!(response.route.as_deref(), Some("local"));
assert_eq!(response.elapsed_ms, Some(4));
}
#[test]
fn a_server_that_never_negotiated_capabilities_grants_none() {
let peer = peer(|stream| {
let _ = read_frame(stream);
let _ = write_frame(stream, TAG_HELLO_OK, r#"{"ok":true,"message":"ok"}"#);
let _ = read_frame(stream);
let _ = write_frame(stream, TAG_AUTH_OK, r#"{"ok":true,"session_id":"s-1"}"#);
let _ = read_frame(stream);
});
let mut db = Client::connect(&peer.options).expect("connect");
assert_eq!(db.granted_features(), 0);
assert!(!db.server_params_granted());
assert!(!db.session_txn_granted());
let error = db
.query_params("SELECT * FROM t WHERE id = ?", ¶ms![1])
.expect_err("binding needs the capability");
assert_eq!(error.kind, ErrorKind::FeatureNotGranted);
assert!(!db.is_poisoned(), "nothing was sent");
}
#[test]
fn the_request_envelope_carries_the_database_and_a_unique_id() {
let peer = peer(|stream| {
handshake(stream);
let (tag, body) = read_frame(stream).expect("a request");
assert_eq!(tag, TAG_REQUEST);
let envelope: serde_json::Value = serde_json::from_slice(&body).expect("json");
assert_eq!(envelope["database"], "reporting");
assert!(envelope["request_id"].as_str().unwrap().starts_with("rs-"));
assert_eq!(envelope["op"]["Sql"]["Query"]["sql"], "SELECT 1");
assert_eq!(envelope["options"]["timeout_ms"], 2500);
let _ = write_frame(
stream,
TAG_RESPONSE,
r#"{"request_id":"r1","status":"ok","data":{"Rows":{"columns":["n"],"rows":[["1"]]}}}"#,
);
let _ = read_frame(stream);
});
let mut options = peer.options.clone();
options.database = "reporting".to_string();
let mut db = Client::connect(&options).expect("connect");
db.set_request_timeout(Some(Duration::from_millis(2500)));
let rows = db.query("SELECT 1").expect("query");
assert_eq!(rows.rows[0][0], "1");
}