yamldap 0.2.0

A lightweight LDAP server that serves directory data from YAML files
Documentation
use std::io::Write;
use std::path::PathBuf;
use tempfile::NamedTempFile;
use tokio::io::AsyncReadExt;
use tokio::net::{TcpListener, TcpStream};
use yamldap::{Config, ResourceLimits, Server};

fn create_test_config(yaml_file: PathBuf) -> Config {
    Config::new(yaml_file)
        .with_bind_address("127.0.0.1:0".parse().unwrap())
        .with_anonymous_access(true)
}

fn create_yaml_file() -> NamedTempFile {
    let mut file = NamedTempFile::new().unwrap();
    writeln!(
        file,
        r#"directory:
  base_dn: dc=test,dc=com

entries:
  - dn: dc=test,dc=com
    objectClass: [top, domain]
    dc: test

  - dn: cn=admin,dc=test,dc=com
    objectClass: [top, person]
    cn: admin
    sn: Admin
    userPassword: secret
"#
    )
    .unwrap();
    file.flush().unwrap();
    file
}

#[tokio::test]
async fn server_starts_accepts_connections_and_shuts_down() {
    let yaml_file = create_yaml_file();
    let server = Server::new(create_test_config(yaml_file.path().to_path_buf()))
        .await
        .unwrap();
    let handle = server.start().await.unwrap();

    TcpStream::connect(handle.local_addr()).await.unwrap();
    handle.shutdown().await.unwrap();
}

#[tokio::test]
async fn server_owns_multiple_connection_tasks_during_shutdown() {
    let yaml_file = create_yaml_file();
    let server = Server::new(create_test_config(yaml_file.path().to_path_buf()))
        .await
        .unwrap();
    let handle = server.start().await.unwrap();

    let mut streams = Vec::new();
    for _ in 0..10 {
        streams.push(TcpStream::connect(handle.local_addr()).await.unwrap());
    }
    drop(streams);

    handle.shutdown().await.unwrap();
}

#[tokio::test]
async fn server_enforces_connection_limit_without_unbounded_tasks() {
    let yaml_file = create_yaml_file();
    let mut limits = ResourceLimits::default();
    limits.max_connections = 1;
    let server = Server::new(create_test_config(yaml_file.path().to_path_buf()))
        .await
        .unwrap()
        .with_resource_limits(limits)
        .unwrap();
    let handle = server.start().await.unwrap();

    let first = TcpStream::connect(handle.local_addr()).await.unwrap();
    let mut second = TcpStream::connect(handle.local_addr()).await.unwrap();
    let mut byte = [0_u8; 1];
    let closed = tokio::time::timeout(std::time::Duration::from_secs(1), second.read(&mut byte))
        .await
        .expect("excess connection was not handled")
        .unwrap();
    assert_eq!(closed, 0, "excess connection should be closed");
    drop(first);
    drop(second);

    handle.shutdown().await.unwrap();
}

#[tokio::test]
async fn server_reports_a_deterministic_bind_error() {
    let yaml_file = create_yaml_file();
    let occupied = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let address = occupied.local_addr().unwrap();
    let config = Config::new(yaml_file.path()).with_bind_address(address);
    let server = Server::new(config).await.unwrap();

    assert!(server.start().await.is_err());
}

#[tokio::test]
async fn server_rejects_a_missing_yaml_file() {
    let config = Config::new(PathBuf::from("/nonexistent/file.yaml"));
    assert!(Server::new(config).await.is_err());
}

#[tokio::test]
async fn server_starts_with_a_custom_schema() {
    let mut file = NamedTempFile::new().unwrap();
    writeln!(
        file,
        r#"directory:
  base_dn: dc=custom,dc=com

schema:
  object_classes:
    - name: customPerson
      attributes: [cn, email]
  custom_attributes:
    email:
      syntax: String
      single_value: true

entries:
  - dn: cn=test,dc=custom,dc=com
    objectClass: [customPerson]
    cn: test
    email: test@example.com
"#
    )
    .unwrap();
    file.flush().unwrap();

    let server = Server::new(create_test_config(file.path().to_path_buf()))
        .await
        .unwrap();
    let handle = server.start().await.unwrap();
    TcpStream::connect(handle.local_addr()).await.unwrap();
    handle.shutdown().await.unwrap();
}