use std::sync::{Arc, Mutex};
use ldap_client::{ClientBuilder, Error, SecretString, parse_range_option};
use ldap_client_ber::{BerWriter, Tag};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
#[test]
fn range_option_mid() {
assert_eq!(
parse_range_option("member;range=0-1499"),
Some(("member", 0, Some(1499)))
);
}
#[test]
fn range_option_final() {
assert_eq!(
parse_range_option("member;range=1500-*"),
Some(("member", 1500, None))
);
}
#[test]
fn range_option_none() {
assert_eq!(parse_range_option("member"), None);
assert_eq!(parse_range_option("member;binary"), None);
}
#[test]
fn range_option_multi_semicolon() {
assert_eq!(
parse_range_option("member;binary;range=0-1499"),
Some(("member", 0, Some(1499)))
);
assert_eq!(
parse_range_option("member;binary;range=1500-*"),
Some(("member", 1500, None))
);
}
const NOTICE_OF_DISCONNECTION_OID: &str = "1.3.6.1.4.1.1466.20036";
fn encode_ldap_message(
message_id: i32,
app_tag: u32,
body_fn: impl FnOnce(&mut BerWriter),
) -> Vec<u8> {
let mut w = BerWriter::new();
w.write_sequence(Tag::sequence(), |msg| {
msg.write_integer(message_id as i64);
msg.write_sequence(Tag::application(app_tag), body_fn);
});
w.into_bytes()
}
fn encode_extended_response(message_id: i32, result_code: i64, oid: Option<&str>) -> Vec<u8> {
encode_ldap_message(message_id, 24, |body| {
body.write_enumerated(result_code);
body.write_bytes(b""); body.write_bytes(b""); if let Some(oid) = oid {
body.write_octet_string(Tag::context(10), oid.as_bytes());
}
})
}
fn encode_bind_response(message_id: i32) -> Vec<u8> {
encode_ldap_message(message_id, 1, |body| {
body.write_enumerated(0);
body.write_bytes(b""); body.write_bytes(b""); })
}
async fn drain_one_message(sock: &mut TcpStream) {
let mut buf = [0u8; 4096];
let n = sock.read(&mut buf).await.unwrap();
assert!(n > 0, "expected an LDAP message");
}
#[tokio::test]
async fn unsolicited_notification_invokes_handler() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let custom_oid = "1.2.3.4.5.6.7.8.9";
let server = tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.unwrap();
drain_one_message(&mut sock).await;
sock.write_all(&encode_extended_response(0, 0, Some(custom_oid)))
.await
.unwrap();
sock.write_all(&encode_bind_response(1)).await.unwrap();
});
let captured: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let captured_clone = captured.clone();
let client = ClientBuilder::new(addr.ip().to_string(), addr.port())
.on_unsolicited_notification(move |resp| {
if let Some(oid) = &resp.oid {
captured_clone.lock().unwrap().push(oid.clone());
}
})
.connect()
.await
.unwrap();
let password = SecretString::from("test");
client.simple_bind("cn=test", &password).await.unwrap();
server.await.unwrap();
let oids = captured.lock().unwrap();
assert_eq!(&*oids, &[custom_oid]);
}
#[tokio::test]
async fn notice_of_disconnection_closes_connection() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.unwrap();
drain_one_message(&mut sock).await;
sock.write_all(&encode_extended_response(
0,
0,
Some(NOTICE_OF_DISCONNECTION_OID),
))
.await
.unwrap();
});
let client = ClientBuilder::new(addr.ip().to_string(), addr.port())
.connect()
.await
.unwrap();
let password = SecretString::from("test");
let err = client.simple_bind("cn=test", &password).await.unwrap_err();
assert!(
matches!(err, Error::ConnectionClosed),
"expected ConnectionClosed, got {err:?}"
);
server.await.unwrap();
}