gfeh-http 0.1.2

The plain-HTTP view of gfeh: per-file public exposure with ranges and conditional requests
Documentation
//! The HTTP view's conformance suite, against every store this crate can be driven with.
//!
//! Three backends, and the point is that all three answer identically:
//!
//! - **`MemStore`**, the reference. What every other backend is held to.
//! - **`LocalStore`**, a real filesystem partition with a real SQLite index. The one
//!   backend whose timestamps come off a disk and whose durability is real.
//! - **`Vfs`**, the composed namespace a daemon actually serves from. A view is served
//!   over composition in production and over a bare store in every other test, and a
//!   composition layer that changed what a `stat` returns would change a header.
//!
//! A case that passes against the reference and fails here names which backend and which
//! behaviour, which is the whole reason the suite is one set of cases rather than three
//! sets of tests.

use gfeh_core::{ObjectStore, Result};
use gfeh_http::conformance::{self, Backend, Target};
use gfeh_store::{LocalStore, MemStore};
use gfeh_vfs::{AllowAll, Vfs};
use std::path::PathBuf;
use std::sync::Arc;

struct Mem;

#[async_trait::async_trait]
impl Backend for Mem {
    fn name(&self) -> String {
        "MemStore".into()
    }

    async fn fresh(&self) -> Result<Target> {
        let store = MemStore::new();
        Ok(Target {
            root: store.root(),
            partition: store.partition(),
            store: Arc::new(store),
        })
    }
}

/// A filesystem partition per case.
///
/// Never `/tmp`: it is tmpfs on most Linux systems, and the index's durability rests on
/// SQLite's WAL reaching a filesystem that has some.
struct Local;

#[async_trait::async_trait]
impl Backend for Local {
    fn name(&self) -> String {
        "LocalStore".into()
    }

    async fn fresh(&self) -> Result<Target> {
        let base = std::env::var("STATE_DIR").map_or_else(
            |_| {
                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                    .join("../../.cache")
                    .join("state-tests")
            },
            PathBuf::from,
        );
        let dir = base
            .join("gfeh-http-conformance")
            .join(format!("pid-{}", std::process::id()))
            .join(uuid::Uuid::new_v4().simple().to_string());
        std::fs::create_dir_all(&dir)
            .map_err(|e| gfeh_core::Error::Storage(format!("scratch: {e}")))?;

        let store = LocalStore::open(&dir)?;
        Ok(Target {
            root: store.root(),
            partition: store.partition(),
            store: Arc::new(store),
        })
    }
}

/// The composed namespace, with nothing mounted.
///
/// What a daemon serves. Every other test in this crate hands the view a bare store, so
/// without this the layer that is actually underneath it in production is the one layer
/// the suite never sees.
struct Composed;

#[async_trait::async_trait]
impl Backend for Composed {
    fn name(&self) -> String {
        "Vfs over MemStore".into()
    }

    async fn fresh(&self) -> Result<Target> {
        let vfs = Vfs::builder(Arc::new(MemStore::new()), Arc::new(AllowAll)).build();
        Ok(Target {
            root: vfs.root(),
            partition: vfs.partition(),
            store: Arc::new(vfs),
        })
    }
}

async fn conformant<B: Backend>(backend: &B) {
    let report = conformance::run(backend).await;
    assert!(report.is_conformant(), "{}", report.summary());
    assert_eq!(
        report.passed(),
        conformance::cases().len(),
        "{}",
        report.summary()
    );
}

#[tokio::test]
async fn the_in_memory_reference_is_conformant() {
    conformant(&Mem).await;
}

#[tokio::test]
async fn a_filesystem_partition_is_conformant() {
    conformant(&Local).await;
}

#[tokio::test]
async fn the_composed_namespace_is_conformant() {
    conformant(&Composed).await;
}

#[tokio::test]
async fn a_failing_backend_is_reported_rather_than_aborting_the_run() {
    // The suite has to be able to say *which* backend is wrong and in how many ways.
    // A runner that stopped at the first failure would turn "this backend disagrees
    // about paging" into one line about one case.
    struct Broken;

    #[async_trait::async_trait]
    impl Backend for Broken {
        fn name(&self) -> String {
            "a store that cannot be provisioned".into()
        }

        async fn fresh(&self) -> Result<Target> {
            Err(gfeh_core::Error::Storage("no store today".into()))
        }
    }

    let report = conformance::run(&Broken).await;
    assert!(!report.is_conformant());
    assert_eq!(report.passed(), 0);
    assert_eq!(report.failures().count(), conformance::cases().len());
    assert!(
        report.summary().contains("no store today"),
        "{}",
        report.summary()
    );
}

#[test]
fn every_case_has_a_distinct_name() {
    // A failure is looked up by name, so two cases sharing one would send somebody to
    // the wrong assertion.
    let mut names: Vec<&str> = conformance::cases().iter().map(|c| c.name()).collect();
    let total = names.len();
    names.sort_unstable();
    names.dedup();
    assert_eq!(names.len(), total, "two cases share a name");
    assert!(total >= 10, "the suite has shrunk to {total} cases");
}

// ---------------------------------------------------------------------------
// A store that does nothing
// ---------------------------------------------------------------------------

/// A store that refuses every operation.
///
/// The three backends above prove the suite passes where it should. This one proves the
/// other half, which is easier to lose: that a case which touches the store *fails* when
/// the store does nothing. A case whose assertions are unreachable -- one that returns
/// early, or swallows the error it meant to check -- passes here, and passing here is
/// exactly what it must not do.
///
/// It also runs the suite's own failure branches, which are otherwise dead code for as
/// long as every backend behaves.
struct Refuses;

fn refused<T>() -> Result<T> {
    Err(gfeh_core::Error::Storage(
        "this store refuses everything".into(),
    ))
}

#[async_trait::async_trait]
impl ObjectStore for Refuses {
    fn partition(&self) -> gfeh_core::PartitionId {
        gfeh_core::PartitionId::new()
    }
    async fn stat(&self, _: &gfeh_core::OpCtx, _: &gfeh_core::NodeRef) -> Result<gfeh_core::Meta> {
        refused()
    }
    async fn lookup(
        &self,
        _: &gfeh_core::OpCtx,
        _: &gfeh_core::NodeRef,
        _: &str,
    ) -> Result<gfeh_core::Meta> {
        refused()
    }
    async fn list(
        &self,
        _: &gfeh_core::OpCtx,
        _: gfeh_core::ListRequest,
    ) -> Result<gfeh_core::ListPage> {
        refused()
    }
    async fn create(
        &self,
        _: &gfeh_core::OpCtx,
        _: &gfeh_core::NodeRef,
        _: &str,
        _: gfeh_core::NodeKind,
        _: gfeh_core::Disposition,
    ) -> Result<Box<dyn gfeh_core::Handle>> {
        refused()
    }
    async fn open(
        &self,
        _: &gfeh_core::OpCtx,
        _: &gfeh_core::NodeRef,
        _: gfeh_core::OpenMode,
    ) -> Result<Box<dyn gfeh_core::Handle>> {
        refused()
    }
    async fn rename(
        &self,
        _: &gfeh_core::OpCtx,
        _: &gfeh_core::NodeRef,
        _: &gfeh_core::NodeRef,
        _: &str,
        _: bool,
    ) -> Result<()> {
        refused()
    }
    async fn remove(
        &self,
        _: &gfeh_core::OpCtx,
        _: &gfeh_core::NodeRef,
        _: gfeh_core::RemoveOpts,
    ) -> Result<()> {
        refused()
    }
    async fn set_meta(
        &self,
        _: &gfeh_core::OpCtx,
        _: &gfeh_core::NodeRef,
        _: gfeh_core::MetaPatch,
    ) -> Result<gfeh_core::Meta> {
        refused()
    }
    async fn begin_multipart(
        &self,
        _: &gfeh_core::OpCtx,
        _: &gfeh_core::NodeRef,
    ) -> Result<gfeh_core::UploadId> {
        refused()
    }
    async fn put_part(
        &self,
        _: &gfeh_core::OpCtx,
        _: &gfeh_core::UploadId,
        _: u32,
        _: gfeh_core::ByteStream,
    ) -> Result<gfeh_core::PartTag> {
        refused()
    }
    async fn complete_multipart(
        &self,
        _: &gfeh_core::OpCtx,
        _: gfeh_core::UploadId,
        _: Vec<gfeh_core::PartTag>,
    ) -> Result<gfeh_core::Meta> {
        refused()
    }
    async fn abort_multipart(&self, _: &gfeh_core::OpCtx, _: gfeh_core::UploadId) -> Result<()> {
        refused()
    }
    async fn changes(
        &self,
        _: &gfeh_core::OpCtx,
        _: gfeh_core::PartitionId,
        _: gfeh_core::ChangeSeq,
        _: usize,
    ) -> Result<gfeh_core::ChangePage> {
        refused()
    }
}

struct DoesNothing;

#[async_trait::async_trait]
impl Backend for DoesNothing {
    fn name(&self) -> String {
        "a store that refuses everything".into()
    }

    async fn fresh(&self) -> Result<Target> {
        let store = MemStore::new();
        Ok(Target {
            root: store.root(),
            partition: store.partition(),
            store: Arc::new(Refuses),
        })
    }
}

#[tokio::test]
async fn only_the_cases_that_never_touch_the_store_survive_one_that_does_nothing() {
    let report = conformance::run(&DoesNothing).await;

    let survivors: Vec<&str> = report
        .outcomes
        .iter()
        .filter(|o| o.failure.is_none())
        .map(|o| o.case)
        .collect();

    // Named exactly rather than counted. These are the cases about the protocol itself --
    // the handshake, the security blob, a status for a handle nobody opened -- which are
    // answered before any store is consulted and so are the only ones entitled to pass
    // here. Anything else appearing in this list is a case that has stopped exercising
    // the store, which is how a suite rots into one that passes vacuously.
    assert_eq!(
        survivors, PROTOCOL_ONLY,
        "the set of cases that pass without a store has changed"
    );
    assert!(!report.is_conformant());
}

/// The cases answered without consulting the store at all: one.
///
/// A token this view was never given names nothing, whatever a store would have said
/// about it. Its sibling -- the case that a withdrawn token is indistinguishable from an
/// unknown one -- has to publish a file first, so it needs a store and fails here like
/// everything else.
const PROTOCOL_ONLY: &[&str] = &["an_unknown_token_is_not_found"];