kcode-web-libs 0.1.0

Managed plain HTML, CSS, and JavaScript libraries with browser checks and immutable publication
Documentation
use kcode_web_libs::{File, Lib, create, docs, open};
use std::fs;
use tempfile::TempDir;

fn contents_mut<'a>(library: &'a mut Lib, path: &str) -> &'a mut String {
    &mut library
        .files
        .iter_mut()
        .find(|file| file.path == path)
        .unwrap()
        .contents
}

#[test]
fn create_open_and_docs_return_the_complete_sorted_source() {
    let root = TempDir::new().unwrap();
    let created = create(root.path(), "Example_lib-2").unwrap();
    assert_eq!(
        created
            .files
            .iter()
            .map(|file| file.path.as_str())
            .collect::<Vec<_>>(),
        ["Documentation.md", "index.js", "kcode-web.json", "tests.js"]
    );

    let opened = open(root.path(), "Example_lib-2").unwrap();
    assert_eq!(opened.files, created.files);
    let (version, documentation) = docs(root.path(), "Example_lib-2").unwrap();
    assert_eq!(version, "0.1.0");
    assert!(documentation.starts_with("# Example_lib-2\n"));
}

#[test]
fn complete_replacement_removes_omitted_files_and_old_generations() {
    let root = TempDir::new().unwrap();
    let mut library = create(root.path(), "demo").unwrap();
    library.files.push(File {
        path: "temporary.js".to_owned(),
        contents: "export {};".to_owned(),
    });
    library.write().unwrap();
    library.files.retain(|file| file.path != "temporary.js");
    *contents_mut(&mut library, "Documentation.md") = "second write\n".to_owned();
    library.write().unwrap();

    let reopened = open(root.path(), "demo").unwrap();
    assert!(
        reopened
            .files
            .iter()
            .all(|file| file.path != "temporary.js")
    );
    assert_eq!(
        reopened
            .files
            .iter()
            .find(|file| file.path == "Documentation.md")
            .unwrap()
            .contents,
        "second write\n"
    );
    assert_eq!(
        fs::read_dir(root.path().join("demo/generations"))
            .unwrap()
            .count(),
        1
    );
}

#[test]
fn two_writers_from_one_snapshot_cannot_both_commit() {
    let root = TempDir::new().unwrap();
    create(root.path(), "demo").unwrap();
    let mut first = open(root.path(), "demo").unwrap();
    let mut second = open(root.path(), "demo").unwrap();
    *contents_mut(&mut first, "Documentation.md") = "first\n".to_owned();
    *contents_mut(&mut second, "Documentation.md") = "second\n".to_owned();

    first.write().unwrap();
    let error = second.write().unwrap_err();
    assert!(error.to_string().starts_with("stale_snapshot:"));
    assert_eq!(docs(root.path(), "demo").unwrap().1, "first\n");
}

#[test]
fn invalid_paths_and_metadata_fail_before_commit() {
    let root = TempDir::new().unwrap();
    let mut library = create(root.path(), "demo").unwrap();
    let original = library.files.clone();

    library.files.push(File {
        path: "../escape".to_owned(),
        contents: "bad".to_owned(),
    });
    assert!(library.write().is_err());
    library.files = original;

    *contents_mut(&mut library, "kcode-web.json") =
        r#"{"name":"demo","version":"1.2.3-beta","entry":"index.js","tests":"tests.js"}"#
            .to_owned();
    assert!(library.write().is_err());
    assert_eq!(docs(root.path(), "demo").unwrap().0, "0.1.0");
}

#[test]
fn check_uses_current_in_memory_source_without_requiring_a_write() {
    let root = TempDir::new().unwrap();
    let mut library = create(root.path(), "demo").unwrap();
    library.files.push(File {
        path: "../not-committed".to_owned(),
        contents: "bad".to_owned(),
    });

    let error = library.check().unwrap_err();
    assert!(error.to_string().starts_with("invalid_source:"));
}

#[test]
fn publish_rechecks_the_current_in_memory_source() {
    let root = TempDir::new().unwrap();
    let mut library = create(root.path(), "demo").unwrap();
    *contents_mut(&mut library, "kcode-web.json") =
        r#"{"name":"demo","version":"1.2.3-beta","entry":"index.js","tests":"tests.js"}"#
            .to_owned();

    let error = library.publish().unwrap_err();
    assert!(error.to_string().starts_with("invalid_source:"));
    assert!(!root.path().join(".published").exists());
}

#[cfg(unix)]
#[test]
fn docs_does_not_traverse_unrelated_source_but_open_rejects_symlinks() {
    use std::os::unix::fs::symlink;

    let root = TempDir::new().unwrap();
    create(root.path(), "demo").unwrap();
    let repository = root.path().join("demo");
    let generation = fs::read_to_string(repository.join("HEAD"))
        .unwrap()
        .trim()
        .to_owned();
    symlink(
        root.path(),
        repository
            .join("generations")
            .join(generation)
            .join("unrelated-link"),
    )
    .unwrap();

    assert_eq!(docs(root.path(), "demo").unwrap().0, "0.1.0");
    assert!(open(root.path(), "demo").is_err());
}