holger-server-lib 0.6.5

Holger server library: config, wiring, gRPC service, Rust API
use std::sync::Arc;

use traits::{ArtifactEntry, ArtifactFormat, ArtifactId, ArchiveInfo, RepositoryBackendTrait};

/// Wraps a primary repository backend with an ordered upstream fallback chain.
///
/// * `fetch` — tries primary, then each upstream in declaration order; returns the
///   first hit found.
/// * `handle_http2_request` — returns the primary's response unless the status is
///   404, in which case it walks the upstream chain and returns the first non-404
///   response (or the primary's 404 if all upstreams also 404).
/// * `put` — always delegates to the primary; upstreams are read-only proxies.
pub struct ProxyBackend {
    primary: Arc<dyn RepositoryBackendTrait>,
    upstreams: Vec<Arc<dyn RepositoryBackendTrait>>,
}

impl ProxyBackend {
    pub fn new(
        primary: Arc<dyn RepositoryBackendTrait>,
        upstreams: Vec<Arc<dyn RepositoryBackendTrait>>,
    ) -> Self {
        Self { primary, upstreams }
    }
}

impl RepositoryBackendTrait for ProxyBackend {
    fn name(&self) -> &str {
        self.primary.name()
    }

    fn format(&self) -> ArtifactFormat {
        self.primary.format()
    }

    fn is_writable(&self) -> bool {
        self.primary.is_writable()
    }

    fn has_archive(&self) -> bool {
        self.primary.has_archive()
    }

    fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> {
        if let Some(data) = self.primary.fetch(id)? {
            return Ok(Some(data));
        }
        for upstream in &self.upstreams {
            if let Some(data) = upstream.fetch(id)? {
                log::debug!(
                    "proxy: cache miss on {}, served from upstream {}",
                    self.primary.name(),
                    upstream.name()
                );
                return Ok(Some(data));
            }
        }
        Ok(None)
    }

    fn put(&self, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()> {
        self.primary.put(id, data)
    }

    fn handle_http2_request(
        &self,
        method: &str,
        suburl: &str,
        body: &[u8],
    ) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
        let resp = self.primary.handle_http2_request(method, suburl, body)?;
        if resp.0 != 404 {
            return Ok(resp);
        }
        for upstream in &self.upstreams {
            let up_resp = upstream.handle_http2_request(method, suburl, body)?;
            if up_resp.0 != 404 {
                log::debug!(
                    "proxy: HTTP {} {} — primary 404, served from upstream {}",
                    method,
                    suburl,
                    upstream.name()
                );
                return Ok(up_resp);
            }
        }
        Ok(resp)
    }

    fn list(
        &self,
        name_filter: Option<&str>,
        limit: usize,
    ) -> anyhow::Result<Vec<ArtifactEntry>> {
        self.primary.list(name_filter, limit)
    }

    fn archive_files(&self, prefix: Option<&str>) -> anyhow::Result<Vec<String>> {
        self.primary.archive_files(prefix)
    }

    fn archive_info(&self) -> anyhow::Result<ArchiveInfo> {
        self.primary.archive_info()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use traits::ArtifactFormat;

    /// Emit one functional-status row for a real check. Gated behind
    /// `--features testmatrix` so release builds strip it (dep is optional).
    #[cfg(feature = "testmatrix")]
    fn fstatus(component: &str, check: &str, ok: bool, detail: &str) {
        nornir_testmatrix::functional_status(component, check, ok, detail);
    }

    struct StubBackend {
        name: String,
        data: std::collections::HashMap<String, Vec<u8>>,
        http_status: u16,
        http_body: Vec<u8>,
        writable: bool,
    }

    impl StubBackend {
        fn always_200(name: &str, body: &[u8]) -> Arc<Self> {
            Arc::new(Self {
                name: name.into(),
                data: Default::default(),
                http_status: 200,
                http_body: body.to_vec(),
                writable: false,
            })
        }
        fn always_404(name: &str) -> Arc<Self> {
            Arc::new(Self {
                name: name.into(),
                data: Default::default(),
                http_status: 404,
                http_body: b"Not found".to_vec(),
                writable: false,
            })
        }
        fn with_artifact(name: &str, crate_name: &str, version: &str, body: &[u8]) -> Arc<Self> {
            let mut data = std::collections::HashMap::new();
            data.insert(format!("{}/{}", crate_name, version), body.to_vec());
            Arc::new(Self {
                name: name.into(),
                data,
                http_status: 404,
                http_body: b"Not found".to_vec(),
                writable: false,
            })
        }
    }

    impl RepositoryBackendTrait for StubBackend {
        fn name(&self) -> &str { &self.name }
        fn format(&self) -> ArtifactFormat { ArtifactFormat::Rust }
        fn is_writable(&self) -> bool { self.writable }

        fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> {
            let key = format!("{}/{}", id.name, id.version);
            Ok(self.data.get(&key).cloned())
        }

        fn put(&self, _: &ArtifactId, _: &[u8]) -> anyhow::Result<()> {
            anyhow::bail!("read-only stub")
        }

        fn handle_http2_request(
            &self,
            _method: &str,
            _suburl: &str,
            _body: &[u8],
        ) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
            Ok((self.http_status, Vec::new(), self.http_body.clone()))
        }
    }

    fn artifact(name: &str, version: &str) -> ArtifactId {
        ArtifactId { namespace: None, name: name.into(), version: version.into() }
    }

    #[test]
    fn fetch_returns_primary_hit_without_consulting_upstreams() {
        let primary = StubBackend::with_artifact("primary", "tokio", "1.0", b"tokio-bytes");
        let upstream = StubBackend::with_artifact("upstream", "tokio", "1.0", b"upstream-bytes");
        let proxy = ProxyBackend::new(primary, vec![upstream]);

        let result = proxy.fetch(&artifact("tokio", "1.0")).unwrap();
        assert_eq!(result.as_deref(), Some(b"tokio-bytes".as_ref()), "primary hit must win");

        #[cfg(feature = "testmatrix")]
        fstatus(
            "proxy",
            "fetch_primary_hit_wins",
            result.as_deref() == Some(b"tokio-bytes".as_ref()),
            &format!("primary hit served {:?} (upstream not consulted)", result.as_deref().map(<[u8]>::len)),
        );
    }

    #[test]
    fn fetch_falls_through_to_upstream_on_primary_miss() {
        let primary = StubBackend::always_404("primary");
        let upstream = StubBackend::with_artifact("upstream", "serde", "1.0", b"serde-bytes");
        let proxy = ProxyBackend::new(primary, vec![upstream]);

        let result = proxy.fetch(&artifact("serde", "1.0")).unwrap();
        assert_eq!(result.as_deref(), Some(b"serde-bytes".as_ref()), "upstream must serve on primary miss");

        #[cfg(feature = "testmatrix")]
        fstatus(
            "proxy",
            "fetch_falls_through_to_upstream",
            result.as_deref() == Some(b"serde-bytes".as_ref()),
            "primary 404 -> upstream served the bytes",
        );
    }

    #[test]
    fn fetch_returns_none_when_all_miss() {
        let primary = StubBackend::always_404("primary");
        let upstream = StubBackend::always_404("upstream");
        let proxy = ProxyBackend::new(primary, vec![upstream]);

        let result = proxy.fetch(&artifact("missing", "1.0")).unwrap();
        assert!(result.is_none());

        #[cfg(feature = "testmatrix")]
        fstatus(
            "proxy",
            "fetch_none_when_all_miss",
            result.is_none(),
            "primary + upstream both 404 -> None",
        );
    }

    #[test]
    fn http_returns_primary_on_non_404() {
        let primary = StubBackend::always_200("primary", b"primary-body");
        let upstream = StubBackend::always_200("upstream", b"upstream-body");
        let proxy = ProxyBackend::new(primary, vec![upstream]);

        let (status, _, body) = proxy.handle_http2_request("GET", "/repo/index/config.json", b"").unwrap();
        assert_eq!(status, 200);
        assert_eq!(body, b"primary-body");

        #[cfg(feature = "testmatrix")]
        fstatus(
            "proxy",
            "http_returns_primary_on_non_404",
            status == 200 && body == b"primary-body",
            &format!("status={status} body=primary-body (upstream skipped)"),
        );
    }

    #[test]
    fn http_falls_through_to_upstream_on_primary_404() {
        let primary = StubBackend::always_404("primary");
        let upstream = StubBackend::always_200("upstream", b"upstream-body");
        let proxy = ProxyBackend::new(primary, vec![upstream]);

        let (status, _, body) = proxy.handle_http2_request("GET", "/repo/index/se/rd/serde", b"").unwrap();
        assert_eq!(status, 200);
        assert_eq!(body, b"upstream-body");

        #[cfg(feature = "testmatrix")]
        fstatus(
            "proxy",
            "http_falls_through_to_upstream_on_404",
            status == 200 && body == b"upstream-body",
            &format!("primary 404 -> upstream status={status} body=upstream-body"),
        );
    }

    #[test]
    fn http_returns_404_when_all_404() {
        let primary = StubBackend::always_404("primary");
        let upstream1 = StubBackend::always_404("upstream1");
        let upstream2 = StubBackend::always_404("upstream2");
        let proxy = ProxyBackend::new(primary, vec![upstream1, upstream2]);

        let (status, _, _) = proxy.handle_http2_request("GET", "/x", b"").unwrap();
        assert_eq!(status, 404);

        #[cfg(feature = "testmatrix")]
        fstatus(
            "proxy",
            "http_404_when_all_404",
            status == 404,
            &format!("primary + 2 upstreams all 404 -> status {status}"),
        );
    }

    #[test]
    fn proxy_name_and_format_delegate_to_primary() {
        let primary = StubBackend::always_404("my-primary-repo");
        let proxy = ProxyBackend::new(primary, vec![]);
        assert_eq!(proxy.name(), "my-primary-repo");
        assert_eq!(proxy.format(), ArtifactFormat::Rust);

        #[cfg(feature = "testmatrix")]
        fstatus(
            "proxy",
            "name_and_format_delegate_to_primary",
            proxy.name() == "my-primary-repo" && proxy.format() == ArtifactFormat::Rust,
            &format!("name={} format={:?}", proxy.name(), proxy.format()),
        );
    }
}