Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-http/tests/front_door.rs

//! **The smart-HTTP listener's other doors: LFS and every refusal**
//! (lane OSSF-SILVER-CODE, 2026-10-31).
//!
//! `tests/admission.rs` drives the git advertisement through a real bound listener; the
//! LFS batch API, the object `GET`/`PUT`/verify and the refusals in
//! front of all of them had no test that went over a socket. Each test here binds a real
//! listener on port 0 with a real in-memory control plane and asserts on what comes back
//! on the wire: the status code, the headers a client reads and the body it parses. The
//! refusals are paired with an admission wherever one exists, so a door that refused
//! everything would fail as surely as one that let everything in.

use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;

use gunnar_core::model::PrincipalKind;
use gunnar_core::{
    memory_control_store, AuthTable, Grant, Level, NewRepo, Principal, RepoCatalog, RepoRef,
    Visibility,
};
use gunnar_http::{Cleartext, GunnarHttp, HttpConfig};
use gunnar_lfs::{Limits, MemLfsStore, Oid};
use gunnar_ssh::seam::{BannerTransport, LfsAccess, LfsTransport, ServiceRequest, TransportError};
use gunnar_ssh::server::Dispatch;
use gunnar_ssh::Control;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
use tokio::net::TcpStream;

/// The writer's token: a grant of `Write` on `team/project`.
const WRITER: &str = "gunnar_pat_WRITERwriterWRITERwriterWRITERwr";
/// The stranger's token: a principal with no grant on anything.
const STRANGER: &str = "gunnar_pat_STRANGERstrangerSTRANGERstrange";

/// `team/project` is Public (anonymous read), `team/secret` is Private; `writer` may
/// push to `team/project`, `stranger` exists and may do nothing.
fn control() -> Control {
    let store = memory_control_store().expect("open the control store");
    let auth = AuthTable::new(Arc::clone(&store)).expect("open the identity table");
    let catalog = RepoCatalog::new(store).expect("open the catalog");
    catalog
        .create_namespace("team")
        .expect("create the namespace");
    let mut public = NewRepo::new(RepoRef::new("team", "project").expect("a legal path"));
    public.visibility = Visibility::Public;
    catalog.create(public).expect("create team/project");
    catalog
        .create(NewRepo::new(
            RepoRef::new("team", "secret").expect("a legal path"),
        ))
        .expect("create team/secret");
    for (id, secret) in [("writer", WRITER), ("stranger", STRANGER)] {
        auth.add_principal(Principal::new(id, id, PrincipalKind::User).expect("a legal id"))
            .expect("add the principal");
        auth.register_token(id, secret, "test", None)
            .expect("register the token");
    }
    auth.grant(Grant::repo(
        "writer",
        RepoRef::new("team", "project").expect("a legal path"),
        Level::Write,
    ))
    .expect("grant write");
    Control::new(auth, catalog)
}

/// An LFS transport over one in-memory store, with the limits under test.
struct MemLfs {
    store: Arc<MemLfsStore>,
    limits: Limits,
}

impl LfsTransport for MemLfs {
    fn open_lfs(&self, _request: &ServiceRequest) -> Result<LfsAccess, TransportError> {
        Ok(LfsAccess {
            store: Arc::clone(&self.store) as Arc<dyn gunnar_lfs::LfsStore>,
            limits: self.limits,
            allowance: None,
        })
    }
}

/// An LFS transport whose store never opens: the shape of a repository on a disk that
/// went away after the ACL said yes.
struct BrokenLfs;

impl LfsTransport for BrokenLfs {
    fn open_lfs(&self, _request: &ServiceRequest) -> Result<LfsAccess, TransportError> {
        Err(TransportError::Unimplemented("the test's lfs store"))
    }
}

/// What a listener is built with.
#[derive(Default)]
struct Wiring {
    lfs: Option<Arc<dyn LfsTransport>>,
    /// `RequireProxyTls` (the default) when false.
    cleartext_allowed: bool,
    anonymous_off: bool,
}

async fn serve(wiring: Wiring) -> SocketAddr {
    let mut server = GunnarHttp::new(
        HttpConfig {
            listen: "127.0.0.1:0".parse().expect("a literal address"),
            cleartext: if wiring.cleartext_allowed {
                Cleartext::Allowed
            } else {
                Cleartext::RequireProxyTls
            },
            allow_anonymous: !wiring.anonymous_off,
            ..HttpConfig::default()
        },
        Dispatch::new(control()),
        Arc::new(BannerTransport::default()),
    );
    if let Some(lfs) = wiring.lfs {
        server = server.with_lfs(lfs);
    }
    let bound = server.bind().await.expect("bind the listener");
    let addr = bound.local_addr();
    tokio::spawn(async move {
        let _ = bound.serve().await;
    });
    addr
}

/// The listener every LFS test talks to: a store with a 64-byte object ceiling and a
/// three-object batch cap, cleartext credentials allowed (a loopback test).
async fn lfs_server() -> (SocketAddr, Arc<MemLfsStore>) {
    let store = Arc::new(MemLfsStore::new());
    let limits = Limits {
        max_object_bytes: 64,
        max_batch_objects: 3,
        ..Limits::default()
    };
    let addr = serve(Wiring {
        lfs: Some(Arc::new(MemLfs {
            store: Arc::clone(&store),
            limits,
        })),
        cleartext_allowed: true,
        ..Wiring::default()
    })
    .await;
    (addr, store)
}

/// One answer off the wire.
#[derive(Debug)]
struct Reply {
    status: u16,
    headers: Vec<(String, String)>,
    body: Vec<u8>,
}

impl Reply {
    fn header(&self, name: &str) -> Option<&str> {
        self.headers
            .iter()
            .find(|(k, _)| k.eq_ignore_ascii_case(name))
            .map(|(_, v)| v.as_str())
    }
    fn text(&self) -> String {
        String::from_utf8_lossy(&self.body).into_owned()
    }
    fn json(&self) -> serde_json::Value {
        serde_json::from_slice(&self.body)
            .unwrap_or_else(|e| panic!("{e}: not JSON: {:?}", self.text()))
    }
}

/// Decode a `Transfer-Encoding: chunked` body.
fn dechunk(mut raw: &[u8]) -> Vec<u8> {
    let mut out = Vec::new();
    loop {
        let Some(end) = raw.windows(2).position(|w| w == b"\r\n") else {
            return out;
        };
        let size = std::str::from_utf8(&raw[..end])
            .ok()
            .and_then(|s| usize::from_str_radix(s.split(';').next()?.trim(), 16).ok())
            .unwrap_or(0);
        if size == 0 {
            return out;
        }
        raw = &raw[end + 2..];
        out.extend_from_slice(&raw[..size.min(raw.len())]);
        raw = raw.get(size + 2..).unwrap_or_default();
    }
}

/// Send one request (`Connection: close`) and read the whole answer.
async fn send(
    addr: SocketAddr,
    method: &str,
    path: &str,
    headers: &[(&str, &str)],
    body: Option<&[u8]>,
) -> Reply {
    let mut head = format!("{method} {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n");
    for (k, v) in headers {
        head.push_str(&format!("{k}: {v}\r\n"));
    }
    let chunked = headers
        .iter()
        .any(|(k, v)| k.eq_ignore_ascii_case("transfer-encoding") && v.contains("chunked"));
    let mut wire = Vec::new();
    match body {
        Some(body) if chunked => {
            wire.extend_from_slice(head.as_bytes());
            wire.extend_from_slice(b"\r\n");
            wire.extend_from_slice(format!("{:x}\r\n", body.len()).as_bytes());
            wire.extend_from_slice(body);
            wire.extend_from_slice(b"\r\n0\r\n\r\n");
        }
        Some(body) => {
            head.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
            wire.extend_from_slice(head.as_bytes());
            wire.extend_from_slice(body);
        }
        None => {
            wire.extend_from_slice(head.as_bytes());
            wire.extend_from_slice(b"\r\n");
        }
    }
    let mut socket = TcpStream::connect(addr).await.expect("connect");
    socket.write_all(&wire).await.expect("write the request");
    let mut raw = Vec::new();
    tokio::time::timeout(Duration::from_secs(20), socket.read_to_end(&mut raw))
        .await
        .expect("the answer ends")
        .expect("read the answer");
    let split = raw
        .windows(4)
        .position(|w| w == b"\r\n\r\n")
        .unwrap_or_else(|| {
            panic!(
                "{method} {path}: no HTTP head in {:?}",
                String::from_utf8_lossy(&raw)
            )
        });
    let head = String::from_utf8_lossy(&raw[..split]).into_owned();
    let mut lines = head.lines();
    let status = lines
        .next()
        .and_then(|l| l.split_whitespace().nth(1))
        .and_then(|c| c.parse().ok())
        .expect("a status line");
    let headers: Vec<(String, String)> = lines
        .filter_map(|l| l.split_once(':'))
        .map(|(k, v)| (k.trim().to_owned(), v.trim().to_owned()))
        .collect();
    let rest = &raw[split + 4..];
    let is_chunked = headers.iter().any(|(k, v)| {
        k.eq_ignore_ascii_case("transfer-encoding") && v.eq_ignore_ascii_case("chunked")
    });
    let body = if is_chunked {
        dechunk(rest)
    } else {
        rest.to_vec()
    };
    Reply {
        status,
        headers,
        body,
    }
}

fn bearer(secret: &str) -> String {
    format!("Bearer {secret}")
}

const LFS_JSON: &str = "application/vnd.git-lfs+json";
const BATCH: &str = "/team/project.git/info/lfs/objects/batch";

async fn batch(addr: SocketAddr, auth: Option<&str>, body: serde_json::Value) -> Reply {
    let bearer = auth.map(bearer);
    let mut headers = vec![("Accept", LFS_JSON), ("Content-Type", LFS_JSON)];
    if let Some(b) = bearer.as_deref() {
        headers.push(("Authorization", b));
    }
    send(
        addr,
        "POST",
        BATCH,
        &headers,
        Some(body.to_string().as_bytes()),
    )
    .await
}

/// **An object goes in and comes out**: the upload batch hands out the `PUT` and the
/// verify actions with the caller's own `Authorization`, the `PUT` stores the bytes,
/// verify says so (and refuses a wrong size), a second upload batch says "already
/// here", and the download batch and the `GET` hand the same bytes back.
#[tokio::test]
async fn an_lfs_object_goes_up_is_verified_and_comes_back_down() {
    let (addr, store) = lfs_server().await;
    let content = b"forty-two bytes of large file, for real!!";
    let oid = Oid::of(content).to_hex();
    let size = content.len();
    let object = format!("/team/project.git/info/lfs/objects/{oid}");

    let up = batch(
        addr,
        Some(WRITER),
        serde_json::json!({ "operation": "upload", "transfers": ["basic"],
            "objects": [{ "oid": oid, "size": size }], "hash_algo": "sha256" }),
    )
    .await;
    assert_eq!(up.status, 200, "{}", up.text());
    assert_eq!(up.header("content-type"), Some(LFS_JSON));
    let answer = up.json();
    assert_eq!(answer["transfer"], "basic");
    assert_eq!(answer["hash_algo"], "sha256");
    let actions = &answer["objects"][0]["actions"];
    assert_eq!(
        actions["upload"]["href"],
        format!("http://localhost/team/project.git/info/lfs/objects/{oid}")
    );
    assert_eq!(actions["upload"]["header"]["Authorization"], bearer(WRITER));
    assert_eq!(
        actions["verify"]["href"],
        "http://localhost/team/project.git/info/lfs/objects/verify"
    );

    let put = send(
        addr,
        "PUT",
        &object,
        &[("Authorization", &bearer(WRITER))],
        Some(content),
    )
    .await;
    assert_eq!(put.status, 200, "{}", put.text());
    assert_eq!(
        gunnar_lfs::LfsStore::size_of(&*store, &Oid::parse(&oid).unwrap()).unwrap(),
        Some(size as u64),
        "the PUT reached the store"
    );

    let verify = |claimed: usize| {
        let body = serde_json::json!({ "oid": oid, "size": claimed }).to_string();
        let auth = bearer(WRITER);
        async move {
            send(
                addr,
                "POST",
                "/team/project.git/info/lfs/objects/verify",
                &[("Authorization", &auth), ("Accept", LFS_JSON)],
                Some(body.as_bytes()),
            )
            .await
        }
    };
    assert_eq!(verify(size).await.status, 200);
    let wrong = verify(size + 1).await;
    assert_eq!(wrong.status, 422, "{}", wrong.text());
    assert!(
        wrong.json()["message"]
            .as_str()
            .unwrap()
            .contains(&format!("is {size} bytes")),
        "{}",
        wrong.text()
    );

    let again = batch(
        addr,
        Some(WRITER),
        serde_json::json!({ "operation": "upload", "objects": [{ "oid": oid, "size": size }] }),
    )
    .await;
    let again = again.json();
    assert!(
        again["objects"][0].get("actions").is_none() && again["objects"][0].get("error").is_none(),
        "an object the server holds is answered with neither actions nor an error: {again}"
    );

    // The download batch is anonymous: team/project is Public.
    let down = batch(
        addr,
        None,
        serde_json::json!({ "operation": "download", "objects": [{ "oid": oid, "size": size }] }),
    )
    .await;
    assert_eq!(down.status, 200, "{}", down.text());
    let href = down.json()["objects"][0]["actions"]["download"]["href"]
        .as_str()
        .expect("a download href")
        .to_owned();
    assert!(href.ends_with(&object), "{href}");

    let got = send(addr, "GET", &object, &[], None).await;
    assert_eq!(got.status, 200, "{}", got.text());
    assert_eq!(got.header("content-type"), Some("application/octet-stream"));
    assert_eq!(
        got.header("content-length"),
        Some(size.to_string().as_str())
    );
    assert_eq!(got.body, content);
}

/// **What the batch API refuses, and how.** A sha1 hash algorithm, a transfer list
/// without `basic`, an unknown operation, a batch over the cap, a body that is not JSON
/// and an `Accept` that is not LFS's: each is refused with the status a client acts on and
/// the LFS error document, never `200` with nothing in it.
#[tokio::test]
async fn the_batch_api_refuses_what_it_cannot_serve() {
    let (addr, _store) = lfs_server().await;
    let oid = Oid::of(b"x").to_hex();

    let sha1 = batch(
        addr,
        None,
        serde_json::json!({ "operation": "download", "objects": [], "hash_algo": "sha1" }),
    )
    .await;
    assert_eq!(sha1.status, 422);
    assert!(sha1.json()["message"].as_str().unwrap().contains("sha256"));
    assert!(sha1.json()["request_id"]
        .as_str()
        .is_some_and(|id| !id.is_empty()));

    let multipart = batch(
        addr,
        None,
        serde_json::json!({ "operation": "download", "transfers": ["multipart-basic"], "objects": [] }),
    )
    .await;
    assert_eq!(multipart.status, 422);
    assert!(
        multipart.text().contains("multipart-basic"),
        "{}",
        multipart.text()
    );

    let many: Vec<_> = (0..4)
        .map(|i| serde_json::json!({ "oid": Oid::of(&[i]).to_hex(), "size": 1 }))
        .collect();
    let over = batch(
        addr,
        None,
        serde_json::json!({ "operation": "download", "objects": many }),
    )
    .await;
    assert_eq!(over.status, 422);
    assert!(over.text().contains("at most 3 objects"), "{}", over.text());

    // Per object, inside a 200: an oid that is not one, an object that is not here, and
    // an upload over the size ceiling.
    let mixed = batch(
        addr,
        Some(WRITER),
        serde_json::json!({ "operation": "upload", "objects": [
            { "oid": "not-an-oid", "size": 1 },
            { "oid": oid, "size": 65 },
        ] }),
    )
    .await;
    assert_eq!(mixed.status, 200, "{}", mixed.text());
    let objects = mixed.json()["objects"].clone();
    assert_eq!(objects[0]["error"]["code"], 422, "{objects}");
    assert_eq!(objects[1]["error"]["code"], 413, "{objects}");
    let missing = batch(
        addr,
        None,
        serde_json::json!({ "operation": "download", "objects": [{ "oid": oid, "size": 1 }] }),
    )
    .await;
    let missing = missing.json();
    assert_eq!(missing["objects"][0]["error"]["code"], 404, "{missing}");
    assert!(missing["objects"][0].get("actions").is_none());

    let garbage = send(
        addr,
        "POST",
        BATCH,
        &[("Accept", LFS_JSON)],
        Some(b"{ this is not json"),
    )
    .await;
    assert_eq!(garbage.status, 400);
    assert!(garbage.text().contains("not valid"), "{}", garbage.text());

    let html = send(
        addr,
        "POST",
        BATCH,
        &[("Accept", "text/html")],
        Some(br#"{"operation":"download","objects":[]}"#),
    )
    .await;
    assert_eq!(html.status, 406);
    assert!(html.text().contains(LFS_JSON), "{}", html.text());

    let no_op = batch(addr, None, serde_json::json!({ "objects": [] })).await;
    assert_eq!(
        no_op.status, 400,
        "a batch with no operation authorises as nothing"
    );
}

/// **Who may move objects.** An anonymous upload batch is a `401` carrying both
/// `LFS-Authenticate` and `WWW-Authenticate`; a stranger's upload is a `404` (the fused
/// sentence); a private repository's download is a `401` anonymously; the writer is
/// admitted. Without a backend the listener says `501`, and a store that will not open
/// after the ACL said yes is a `503`.
#[tokio::test]
async fn lfs_access_follows_the_acl_and_the_wiring() {
    let (addr, _store) = lfs_server().await;
    let upload = serde_json::json!({ "operation": "upload", "objects": [] });

    let anonymous = batch(addr, None, upload.clone()).await;
    assert_eq!(anonymous.status, 401, "{}", anonymous.text());
    assert_eq!(
        anonymous.header("lfs-authenticate"),
        Some("Basic realm=\"gunnar\"")
    );
    assert_eq!(
        anonymous.header("www-authenticate"),
        Some("Basic realm=\"gunnar\"")
    );

    let stranger = batch(addr, Some(STRANGER), upload.clone()).await;
    assert_eq!(stranger.status, 404, "{}", stranger.text());
    assert!(stranger.header("lfs-authenticate").is_none());

    assert_eq!(batch(addr, Some(WRITER), upload.clone()).await.status, 200);

    let private = send(
        addr,
        "POST",
        "/team/secret.git/info/lfs/objects/batch",
        &[("Accept", LFS_JSON)],
        Some(br#"{"operation":"download","objects":[]}"#),
    )
    .await;
    assert_eq!(private.status, 401, "{}", private.text());

    let anonymous_put = send(
        addr,
        "PUT",
        &format!(
            "/team/project.git/info/lfs/objects/{}",
            Oid::of(b"y").to_hex()
        ),
        &[],
        Some(b"y"),
    )
    .await;
    assert_eq!(
        anonymous_put.status, 401,
        "a PUT is an upload whatever the batch said"
    );

    let unwired = serve(Wiring {
        cleartext_allowed: true,
        ..Wiring::default()
    })
    .await;
    let no_lfs = batch(unwired, None, upload.clone()).await;
    assert_eq!(no_lfs.status, 501);
    assert!(
        no_lfs.text().contains("no LFS object store"),
        "{}",
        no_lfs.text()
    );

    let broken = serve(Wiring {
        lfs: Some(Arc::new(BrokenLfs)),
        cleartext_allowed: true,
        ..Wiring::default()
    })
    .await;
    let down = batch(
        broken,
        None,
        serde_json::json!({ "operation": "download", "objects": [] }),
    )
    .await;
    assert_eq!(down.status, 503, "{}", down.text());
    let object = format!(
        "/team/project.git/info/lfs/objects/{}",
        Oid::of(b"z").to_hex()
    );
    assert_eq!(send(broken, "GET", &object, &[], None).await.status, 503);
}

/// **The object doors refuse before they store.** An oid that is not one is a `422`
/// on `GET`, `PUT` and verify alike; a `PUT` with no `Content-Length` is a `411`; one
/// declaring more than the ceiling is a `413` before a byte is read; bytes that do not
/// hash to the oid are not committed; a `GET` and a verify of an object the server does
/// not hold are `404`s.
#[tokio::test]
async fn the_object_doors_refuse_before_they_store() {
    let (addr, store) = lfs_server().await;
    let auth = bearer(WRITER);
    let auth = [("Authorization", auth.as_str())];

    let bad = send(
        addr,
        "GET",
        "/team/project.git/info/lfs/objects/abc",
        &[],
        None,
    )
    .await;
    assert_eq!(bad.status, 422, "{}", bad.text());
    let bad_put = send(
        addr,
        "PUT",
        "/team/project.git/info/lfs/objects/abc",
        &auth,
        Some(b"abc"),
    )
    .await;
    assert_eq!(bad_put.status, 422, "{}", bad_put.text());
    let bad_verify = send(
        addr,
        "POST",
        "/team/project.git/info/lfs/objects/verify",
        &auth,
        Some(br#"{"oid":"abc","size":3}"#),
    )
    .await;
    assert_eq!(bad_verify.status, 422, "{}", bad_verify.text());
    let not_json = send(
        addr,
        "POST",
        "/team/project.git/info/lfs/objects/verify",
        &auth,
        Some(b"[]"),
    )
    .await;
    assert_eq!(not_json.status, 400, "{}", not_json.text());

    let content = b"small";
    let oid = Oid::of(content).to_hex();
    let object = format!("/team/project.git/info/lfs/objects/{oid}");
    let chunked = send(
        addr,
        "PUT",
        &object,
        &[auth[0], ("Transfer-Encoding", "chunked")],
        Some(content),
    )
    .await;
    assert_eq!(chunked.status, 411, "{}", chunked.text());

    let big = vec![7u8; 65];
    let big_oid = Oid::of(&big).to_hex();
    let too_big = send(
        addr,
        "PUT",
        &format!("/team/project.git/info/lfs/objects/{big_oid}"),
        &auth,
        Some(&big),
    )
    .await;
    assert_eq!(too_big.status, 413, "{}", too_big.text());
    assert!(too_big.text().contains("at most 64"), "{}", too_big.text());

    let lie = send(addr, "PUT", &object, &auth, Some(b"other")).await;
    assert!(
        lie.status >= 400 && lie.status < 500,
        "bytes that are not the oid's are refused: {} {}",
        lie.status,
        lie.text()
    );
    let oid_value = Oid::parse(&oid).unwrap();
    assert_eq!(
        gunnar_lfs::LfsStore::size_of(&*store, &oid_value).unwrap(),
        None,
        "a refused upload left nothing behind"
    );

    let missing = send(addr, "GET", &object, &[], None).await;
    assert_eq!(missing.status, 404, "{}", missing.text());
    let unverified = send(
        addr,
        "POST",
        "/team/project.git/info/lfs/objects/verify",
        &auth,
        Some(format!(r#"{{"oid":"{oid}","size":5}}"#).as_bytes()),
    )
    .await;
    assert_eq!(unverified.status, 404, "{}", unverified.text());
}

/// **The front door's own refusals.** `/healthz` answers without a credential; a dumb
/// HTTP request is a `403` and a path that is not git a `404`; an `Authorization` that is
/// neither Basic nor Bearer is a `401`, an unknown token a `401`, and any token over a
/// plain connection a `403` unless the operator allowed cleartext; with anonymous access
/// off, a keyless request is a `401`; a private repository is `401` anonymously and `404`
/// to a principal with no grant; a gzipped body that is not gzip is a `400`.
#[tokio::test]
async fn the_front_door_refuses_by_name() {
    let strict = serve(Wiring::default()).await;
    let health = send(strict, "GET", "/healthz", &[], None).await;
    assert_eq!((health.status, health.text().trim()), (200, "ok"));
    let refs = "/team/project.git/info/refs?service=git-upload-pack";
    assert_eq!(send(strict, "GET", refs, &[], None).await.status, 200);
    let dumb = send(strict, "GET", "/team/project.git/info/refs", &[], None).await;
    assert_eq!(dumb.status, 403, "{}", dumb.text());
    assert_eq!(
        send(strict, "GET", "/nothing/here", &[], None).await.status,
        404
    );

    let digest = send(
        strict,
        "GET",
        refs,
        &[("Authorization", "Digest abc")],
        None,
    )
    .await;
    assert_eq!(digest.status, 401, "{}", digest.text());
    assert_eq!(
        digest.header("www-authenticate"),
        Some("Basic realm=\"gunnar\"")
    );
    let cleartext = send(
        strict,
        "GET",
        refs,
        &[("Authorization", &bearer(WRITER))],
        None,
    )
    .await;
    assert_eq!(cleartext.status, 403, "{}", cleartext.text());
    let behind_tls = send(
        strict,
        "GET",
        refs,
        &[
            ("Authorization", &bearer(WRITER)),
            ("X-Forwarded-Proto", "https"),
        ],
        None,
    )
    .await;
    assert_eq!(behind_tls.status, 200, "a proxy that spoke TLS is believed");

    let open = serve(Wiring {
        cleartext_allowed: true,
        ..Wiring::default()
    })
    .await;
    let unknown = send(
        open,
        "GET",
        refs,
        &[(
            "Authorization",
            "Bearer gunnar_pat_nobodyNOBODYnobodyNOBODYnobody",
        )],
        None,
    )
    .await;
    assert_eq!(unknown.status, 401, "{}", unknown.text());
    let secret = "/team/secret.git/info/refs?service=git-upload-pack";
    assert_eq!(send(open, "GET", secret, &[], None).await.status, 401);
    let stranger = send(
        open,
        "GET",
        secret,
        &[("Authorization", &bearer(STRANGER))],
        None,
    )
    .await;
    assert_eq!(stranger.status, 404, "{}", stranger.text());

    let gz = send(
        open,
        "POST",
        "/team/project.git/git-upload-pack",
        &[("Content-Encoding", "gzip")],
        Some(b"this is not gzip at all"),
    )
    .await;
    assert_eq!(gz.status, 400, "{}", gz.text());
    assert!(gz.text().contains("gzipped"), "{}", gz.text());

    let closed = serve(Wiring {
        anonymous_off: true,
        ..Wiring::default()
    })
    .await;
    let keyless = send(closed, "GET", refs, &[], None).await;
    assert_eq!(keyless.status, 401, "{}", keyless.text());
}