audis 0.2.1

An audit logging system, built atop Redis.
Documentation
use audis;

use rand;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};

use std::fs;
use std::process;
use std::thread::sleep;
use std::time::Duration;

struct RedisServer {
    process: process::Child,
    url: String,
    path: String,
}

impl RedisServer {
    fn new() -> RedisServer {
        let mut cmd = process::Command::new("redis-server");
        cmd.stdout(process::Stdio::null())
            .stderr(process::Stdio::null());

        let path = {
            let (a, b) = rand::random::<(u64, u64)>();
            let path = format!("/tmp/redis-rs-test-{}-{}.sock", a, b);
            cmd.arg("--port").arg("0").arg("--unixsocket").arg(&path);
            path
        };

        let url = format!("unix:{}", path);
        let process = cmd.spawn().unwrap();
        RedisServer { process, path, url }
    }

    fn stop(&mut self) {
        let _ = self.process.kill();
        let _ = self.process.wait();
        fs::remove_file(&self.path).ok();
    }
}

impl Drop for RedisServer {
    fn drop(&mut self) {
        self.stop()
    }
}

fn server() -> (RedisServer, audis::Client) {
    let s = RedisServer::new();
    let c;

    let ms = Duration::from_millis(1);
    loop {
        match audis::Client::connect(&s.url) {
            Err(err) => {
                if err.is_connection_refusal() {
                    println!("trying to connect; failing.  sleeping for 1ms");
                    sleep(ms);
                } else {
                    panic!("Could not connect: {}", err);
                }
            }
            Ok(con) => {
                c = con;
                break;
            }
        };
    }

    (s, c)
}

fn id() -> String {
    thread_rng().sample_iter(&Alphanumeric).take(30).collect()
}

#[test]
fn it_indexes_across_multiple_subjects() {
    let (s, c) = server();

    let id1 = id();
    c.log(&audis::Event {
        id: id1.to_string(),
        data: "{id1 data}".to_string(),
        subjects: vec!["system".to_string(), "user:42".to_string()],
    })
    .unwrap();

    let log = c.retrieve("system").unwrap();
    assert_eq!(log.len(), 1);
    assert_eq!(log[0].id, id1);

    let log = c.retrieve("user:42").unwrap();
    assert_eq!(log.len(), 1);
    assert_eq!(log[0].id, id1);

    let log = c.retrieve("enoent").unwrap();
    assert_eq!(log.len(), 0);

    drop(s);
}

#[test]
fn it_inserts_audit_events_in_order() {
    let (s, c) = server();

    let ids = vec![id(), id(), id()];
    let subj = vec!["all".to_string()];

    let log = c.retrieve(&subj[0]).unwrap();
    assert_eq!(log.len(), 0);

    for id in &ids {
        c.log(&audis::Event {
            id: id.to_string(),
            data: format!("[{} data]", id),
            subjects: subj.clone(),
        })
        .unwrap();
    }

    let log = c.retrieve(&subj[0]).unwrap();
    assert_eq!(log.len(), 3);
    assert_eq!(log[0].id, ids[0]);
    assert_eq!(log[1].id, ids[1]);
    assert_eq!(log[2].id, ids[2]);

    drop(s);
}

#[test]
fn it_can_function_in_a_background_thread() {
    let (s, c) = server();

    let ids = vec![id(), id(), id()];
    let subj = vec!["all".to_string()];

    let log = c.retrieve(&subj[0]).unwrap();
    assert_eq!(log.len(), 0);

    let (tx, tid) = c.background(2).unwrap();

    for id in &ids {
        tx.send(audis::Event {
            id: id.to_string(),
            data: format!("[{} data]", id),
            subjects: subj.clone(),
        })
        .unwrap();
    }
    drop(tx);
    tid.join().unwrap();

    let log = c.retrieve(&subj[0]).unwrap();
    assert_eq!(log.len(), 3);
    assert_eq!(log[0].id, ids[0]);
    assert_eq!(log[1].id, ids[1]);
    assert_eq!(log[2].id, ids[2]);

    drop(s);
}

#[test]
fn it_truncates_log_indices() {
    let (s, c) = server();

    let ids = vec![id(), id(), id()];
    let subj = vec!["all".to_string()];

    let log = c.retrieve(&subj[0]).unwrap();
    assert_eq!(log.len(), 0);

    for id in &ids {
        c.log(&audis::Event {
            id: id.to_string(),
            data: format!("[{} data]", id),
            subjects: subj.clone(),
        })
        .unwrap();
    }

    let log = c.retrieve(&subj[0]).unwrap();
    assert_eq!(log.len(), 3);
    assert_eq!(log[0].id, ids[0]);
    assert_eq!(log[1].id, ids[1]);
    assert_eq!(log[2].id, ids[2]);

    c.truncate(&subj[0], 2).unwrap();
    let log = c.retrieve(&subj[0]).unwrap();
    assert_eq!(log.len(), 2);
    assert_eq!(log[0].id, ids[1]);
    assert_eq!(log[1].id, ids[2]);

    drop(s);
}

#[test]
fn it_purges_logs() {
    let (s, c) = server();

    let ids = vec![id(), id(), id()];
    let subj = vec!["all".to_string()];

    let log = c.retrieve(&subj[0]).unwrap();
    assert_eq!(log.len(), 0);

    for id in &ids {
        c.log(&audis::Event {
            id: id.to_string(),
            data: format!("[{} data]", id),
            subjects: subj.clone(),
        })
        .unwrap();
    }

    let log = c.retrieve(&subj[0]).unwrap();
    assert_eq!(log.len(), 3);
    assert_eq!(log[0].id, ids[0]);
    assert_eq!(log[1].id, ids[1]);
    assert_eq!(log[2].id, ids[2]);

    c.purge(&subj[0], &ids[1]).unwrap();
    let log = c.retrieve(&subj[0]).unwrap();
    assert_eq!(log.len(), 1);
    assert_eq!(log[0].id, ids[2]);

    drop(s);
}

#[test]
#[should_panic(expected = "duplicate key detected")]
fn it_cannot_insert_duplicate_event_ids() {
    let (s, c) = server();

    let id = id();
    let subj = vec!["dup".to_string()];

    c.log(&audis::Event {
        id: id.to_string(),
        data: format!("[{} data]", id),
        subjects: subj.clone(),
    })
    .unwrap();

    let log = c.retrieve(&subj[0]).unwrap();
    assert_eq!(log.len(), 1);
    assert_eq!(log[0].id, id);

    c.log(&audis::Event {
        id: id.to_string(),
        data: format!("[{} data]", id),
        subjects: subj.clone(),
    })
    .unwrap();

    // this is here to catch failures to fail...
    let log = c.retrieve(&subj[0]).unwrap();
    assert_eq!(log.len(), 1);
    assert_eq!(log[0].id, id);

    drop(s);
}