file-with-meta 0.2.0

store a file's metadata for caching purposes
Documentation
/*
 * Copyright (c) 2021, 2022  Peter Pentchev <roam@ringlet.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
//! Tests for the file-with-meta library.
// This is a test suite, right?
#![allow(clippy::panic_in_result_fn)]
#![allow(clippy::print_stdout)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::use_debug)]

use std::fs;
use std::path::Path;

use anyhow::{Context, Result};

use super::{Error as MetadataError, FileHttpMetadata};

#[test]
#[allow(clippy::shadow_reuse)]
#[allow(clippy::shadow_unrelated)]
fn test_simple() -> Result<()> {
    let tempd_obj = tempfile::tempdir().context("Could not create a temporary directory")?;
    let tempd: &Path = tempd_obj.as_ref();
    println!("Using tempd {}", tempd.to_string_lossy());

    let path = tempd.join("triv-data.dat");
    let path_meta = tempd.join("triv-data.dat.metatriv");

    println!("match_meta() should return None if there are no files");
    assert!(super::match_meta(&path, &path_meta)?.is_none());

    println!("Creating the data file...");
    fs::write(&path, "Hello!\n").context("Could not write to the temporary file")?;

    println!("match_meta() should return None if there is no meta file");
    assert!(super::match_meta(&path, &path_meta)?.is_none());

    println!("from_file() should return a valid metadata object");
    let meta = FileHttpMetadata::from_file(&path)
        .context("Could not obtain metadata from the temporary file")?;

    println!("from_file() returned {:?}", meta);

    println!("match_meta() should still return None");
    assert!(super::match_meta(&path, &path_meta)?.is_none());

    println!("Creating the metadata file");
    fs::write(
        &path_meta,
        serde_json::to_string(&meta).context("Could not serialize the metadata")?,
    )
    .context("Could not write the metadata file")?;

    println!("match_meta() should now return something real");
    let meta_read =
        super::match_meta(&path, &path_meta)?.context("match_meta() should return something")?;

    println!("The two metadata objects should be exactly the same");
    assert_eq!(meta, meta_read);

    println!("Writing modified metadata");
    let meta_mod = FileHttpMetadata {
        file_size: meta.file_size + 1,
        ..meta_read
    };
    fs::write(
        &path_meta,
        serde_json::to_string(&meta_mod).context("Could not serialize the metadata")?,
    )
    .context("Could not write the metadata file")?;

    println!("match_meta() should now return None again");
    assert!(super::match_meta(&path, &path_meta)?.is_none());

    println!("Writing the original metadata back");
    fs::write(
        &path_meta,
        serde_json::to_string(&meta).context("Could not serialize the metadata")?,
    )
    .context("Could not write the metadata file")?;

    println!("match_meta() should, once again, return something useful");
    assert_eq!(
        meta,
        super::match_meta(&path, &path_meta)?.context("match_meta() should return something")?
    );

    println!("Modifying a header field should not cause match_meta() to return None");
    let meta_mod = FileHttpMetadata {
        file_size: meta_mod.file_size - 1,
        hdr_last_modified: Some("hello there".to_owned()),
        ..meta_mod
    };
    fs::write(
        &path_meta,
        serde_json::to_string(&meta_mod).context("Could not serialize the metadata")?,
    )
    .context("Could not write the metadata file")?;
    let meta_read =
        super::match_meta(&path, &path_meta)?.context("match_meta() should return something")?;
    assert_ne!(meta, meta_read);
    let meta_read = FileHttpMetadata {
        hdr_last_modified: None,
        ..meta_read
    };
    assert_eq!(meta, meta_read);

    println!("Removing the data file");
    fs::remove_file(&path).context("Could not remove the temporary file")?;

    println!("match_meta() should return None again");
    assert!(super::match_meta(&path, &path_meta)?.is_none());

    println!("Looks like we reached the end of the trivial test sequence");
    Ok(())
}

#[test]
#[allow(clippy::shadow_reuse)]
#[allow(clippy::shadow_unrelated)]
fn test_source() -> Result<()> {
    let tempd_obj = tempfile::tempdir().context("Could not create a temporary directory")?;
    let tempd: &Path = tempd_obj.as_ref();
    println!("Using tempd {}", tempd.to_string_lossy());

    let path = tempd.join("src-data.dat");
    let path_meta = tempd.join("src-data.dat.metasrc");

    let path_source = tempd.join("src-src.dat");

    println!("match...source() should return empty with no files");
    assert!(super::match_meta_with_source(&path, &path_meta, &path_source)?.is_none());

    println!("from..source() should error out with no files");
    FileHttpMetadata::from_file_with_source(&path, &path_source).unwrap_err();

    println!("match...source() should return empty with no meta file");
    assert!(super::match_meta_with_source(&path, &path_meta, &path_source)?.is_none());
    fs::write(&path, "Hi there!").context("Could not create the temporary file")?;

    println!("match...source() should return something even with no source file");
    let meta = FileHttpMetadata::from_file(&path)
        .context("Could not get the metadata for the temporary file")?;
    assert!(meta.source_file_size.is_none());
    assert!(meta.source_file_mtime.is_none());
    fs::write(
        &path_meta,
        serde_json::to_string(&meta).context("Could not serialize the metadata")?,
    )
    .context("Could not write the metadata file")?;

    let meta_read = super::match_meta_with_source(&path, &path_meta, &path_source)?
        .context("match..source() with no source should still succeed")?;
    assert_eq!(meta, meta_read);

    println!("Now let us see what happens if we create a source file...");
    fs::write(&path_source, "Something something something source file")
        .context("Could not create the source file")?;
    let meta_source = FileHttpMetadata::from_file(&path_source)
        .context("Could not get the metadata for the source file")?;
    let meta = FileHttpMetadata::from_file_with_source(&path, &path_source)
        .context("Could not get the metadata for the temporary and source files")?;
    assert_eq!(meta.source_file_size, Some(meta_source.file_size));
    assert_eq!(meta.source_file_mtime, Some(meta_source.file_mtime));
    assert_ne!(meta, meta_read);
    assert_eq!(
        meta,
        FileHttpMetadata {
            source_file_size: Some(meta_source.file_size),
            source_file_mtime: Some(meta_source.file_mtime),
            ..meta_read
        }
    );

    println!("Making sure match..with_source() also works");
    fs::write(
        &path_meta,
        serde_json::to_string(&meta).context("Could not serialize the metadata")?,
    )
    .context("Could not write to the metadata file")?;
    let meta_read = super::match_meta_with_source(&path, &path_meta, &path_source)?
        .context("match..source() with no source should still succeed")?;
    assert_eq!(meta, meta_read);

    println!("Now let us remove the source file and see what happens");
    fs::remove_file(&path_source).context("Could not remove the source file")?;

    println!("match..source() should return None");
    // match..source() should return None with no source file and recorded data
    assert!(super::match_meta_with_source(&path, &path_meta, &path_source)?.is_none());

    println!("from..source() should error out");
    FileHttpMetadata::from_file_with_source(&path, &path_source).unwrap_err();

    println!("from..source_meta() should still succeed");
    let meta_read = FileHttpMetadata::from_file_with_source_meta(&path, &meta_source)
        .context("Could not examine the temporary and missing source file")?;
    assert_eq!(meta, meta_read);

    println!("Replacing the metadata, forgetting about the source part");
    let meta_read = FileHttpMetadata {
        source_file_size: None,
        source_file_mtime: None,
        ..meta_read
    };
    fs::write(
        &path_meta,
        serde_json::to_string(&meta_read).context("Could not serialize the metadata")?,
    )
    .context("Could not write to the metadata file")?;
    let meta_read = super::match_meta_with_source(&path, &path_meta, &path_source)?
        .context("match..source() with no source should still succeed")?;
    assert_ne!(meta, meta_read);
    assert_eq!(
        meta,
        FileHttpMetadata {
            source_file_size: Some(meta_source.file_size),
            source_file_mtime: Some(meta_source.file_mtime),
            ..meta_read
        }
    );

    println!("Done with the source test sequence");
    Ok(())
}

#[test]
fn test_default() {
    let meta = FileHttpMetadata::default();
    println!("default meta: {:?}", meta);

    assert_eq!(meta.format.version.major, 0);
    assert_eq!(meta.format.version.minor, 1);
    assert_eq!(meta.file_size, 0);
    assert_eq!(meta.file_mtime, 0);
    assert!(meta.source_file_size.is_none());
    assert!(meta.source_file_mtime.is_none());
}

#[test]
const fn test_send() {
    const fn assert_send<T: Send>() {}

    assert_send::<FileHttpMetadata>();
    assert_send::<MetadataError>();
}

#[test]
const fn test_sync() {
    const fn assert_sync<T: Sync>() {}

    assert_sync::<FileHttpMetadata>();
    assert_sync::<MetadataError>();
}

#[cfg(feature = "ureq")]
#[test]
fn test_build_req() -> Result<()> {
    let tempd_obj = tempfile::tempdir().context("Could not create a temporary directory")?;
    let tempd: &Path = tempd_obj.as_ref();

    let path = tempd.join("triv-data.dat");
    let path_meta = tempd.join("triv-data.dat.metatriv");

    fs::write(&path, "this is a test").context("Could not create the temporary file")?;
    let meta_raw = FileHttpMetadata::from_file(&path)
        .context("Could not get the metadata for the temporary file")?;
    assert_eq!(meta_raw.file_size, 14);
    assert!(meta_raw.file_mtime > 0);

    let req = ureq::get("https://example.com/");
    assert!(req.header("If-None-Match").is_none());
    assert!(req.header("If-Modified-Since").is_none());

    let meta_last_mod = FileHttpMetadata {
        hdr_last_modified: Some("today".to_owned()),
        ..meta_raw
    };
    fs::write(
        &path_meta,
        serde_json::to_string(&meta_last_mod)
            .context("Could not serialize the metadata")?
            .as_bytes(),
    )
    .context("Could not write to the metadata file")?;
    assert_eq!(
        super::match_meta(&path, &path_meta)?.unwrap(),
        meta_last_mod
    );
    {
        let (req_last_mod, meta_req_last_mod) = super::build_req(req.clone(), &path, &path_meta)?;
        assert_eq!(meta_last_mod, meta_req_last_mod.unwrap());
        assert!(req_last_mod.header("If-None-Match").is_none());
        assert_eq!(req_last_mod.header("If-Modified-Since").unwrap(), "today");
    }

    let meta_etag_mod = FileHttpMetadata {
        hdr_etag: Some("tagtag!".to_owned()),
        ..meta_last_mod
    };
    fs::write(
        &path_meta,
        serde_json::to_string(&meta_etag_mod)
            .context("Could not serialize the metadata")?
            .as_bytes(),
    )
    .context("Could not write to the metadata file")?;
    assert_eq!(
        super::match_meta(&path, &path_meta)?.unwrap(),
        meta_etag_mod
    );
    {
        let (req_etag_mod, meta_req_etag_mod) = super::build_req(req.clone(), &path, &path_meta)?;
        assert_eq!(meta_etag_mod, meta_req_etag_mod.unwrap());
        assert_eq!(req_etag_mod.header("If-None-Match").unwrap(), "tagtag!");
        assert!(req_etag_mod.header("If-Modified-Since").is_none());
    }

    let meta_etag_only = FileHttpMetadata {
        hdr_last_modified: None,
        ..meta_etag_mod
    };
    fs::write(
        &path_meta,
        serde_json::to_string(&meta_etag_only)
            .context("Could not serialize the metadata")?
            .as_bytes(),
    )
    .context("Could not write to the metadata file")?;
    assert_eq!(
        super::match_meta(&path, &path_meta)?.unwrap(),
        meta_etag_only
    );
    {
        let (req_etag_only, meta_req_etag_only) = super::build_req(req, &path, &path_meta)?;
        assert_eq!(meta_etag_only, meta_req_etag_only.unwrap());
        assert_eq!(req_etag_only.header("If-None-Match").unwrap(), "tagtag!");
        assert!(req_etag_only.header("If-Modified-Since").is_none());
    }
    Ok(())
}