~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
929 lines · 35.9 KB Raw History
//! `/install.sh` and `/releases`: the script, run as `curl … | sh` runs it,
//! installing from a release laid out as `deploy/release.sh` lays one out,
//! or published as a repository's.

#[path = "../../knotless/tests/common/mod.rs"]
mod common;
use common::*;

use knotless_serve::releases::Source;
use sha2::{Digest, Sha256};
use std::path::Path;
use std::process::{Command, Output};
use std::sync::{Arc, Mutex};

const BINS: [&str; 7] = [
    "knotless",
    "git-remote-knotless",
    "knotless-mcp",
    "knotless-run",
    "knotless-triage",
    "knotless-agent",
    "knotless-claude-review",
];

/// What a release from before the runner, labeller and reviewer shipped
/// holds.
const OLD_BINS: usize = 3;

/// The binaries a release of `version` holds: 0.1.0 is one from before
/// the rest shipped.
fn bins(version: &str) -> &'static [&'static str] {
    if version == "0.1.0" {
        &BINS[..OLD_BINS]
    } else {
        &BINS
    }
}

/// The target this machine's script asks for.
fn target() -> String {
    let os = match std::env::consts::OS {
        "macos" => "apple-darwin",
        _ => "unknown-linux-musl",
    };
    format!("{}-{os}", std::env::consts::ARCH)
}

/// This machine's archive of `version` in `out`, its binaries scripts that
/// say who they are, staged in `dir`.
fn archive(dir: &Path, out: &Path, version: &str) -> std::path::PathBuf {
    let name = format!("knotless-{version}-{}", target());
    let staged = dir.join("staging").join(&name);
    std::fs::create_dir_all(&staged).unwrap();
    for bin in bins(version) {
        std::fs::write(staged.join(bin), format!("#!/bin/sh\necho {bin}\n")).unwrap();
    }
    std::fs::create_dir_all(out).unwrap();
    let archive = out.join(format!("{name}.tar.gz"));
    let tar = Command::new("tar")
        .arg("-czf")
        .arg(&archive)
        .arg("-C")
        .arg(dir.join("staging"))
        .arg(&name)
        .status()
        .unwrap();
    assert!(tar.success());
    archive
}

/// A release of `version` in `dir`.
fn release(dir: &Path, version: &str) {
    let name = format!("knotless-{version}-{}", target());
    let out = dir.join("releases").join(version);
    let archive = archive(dir, &out, version);
    let sum = format!("{:x}", Sha256::digest(std::fs::read(&archive).unwrap()));
    std::fs::write(
        out.join(format!("{name}.tar.gz.sha256")),
        format!("{sum}  {name}.tar.gz\n"),
    )
    .unwrap();
    std::fs::write(dir.join("releases/latest"), format!("{version}\n")).unwrap();
}

struct Server {
    base: String,
    _rt: tokio::runtime::Runtime,
}

fn serve(dir: Option<&Path>, public_url: Option<&str>) -> Server {
    let source = dir.map(|d| Source::Dir(d.join("releases")));
    let ctx = knotless::env::Ctx::new(knotless::env::Env::from_vars(|_| None));
    serve_from(source, public_url, ctx)
}

fn serve_from(source: Option<Source>, public_url: Option<&str>, ctx: knotless::env::Ctx) -> Server {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let listener = rt
        .block_on(tokio::net::TcpListener::bind("127.0.0.1:0"))
        .unwrap();
    let base = format!("http://{}", listener.local_addr().unwrap());
    let router = knotless_serve::releases::routes(source, public_url.map(String::from), ctx);
    rt.spawn(async move { axum::serve(listener, router).await.unwrap() });
    Server { base, _rt: rt }
}

fn get(url: &str, headers: &[(&str, &str)]) -> (u16, String) {
    let client = reqwest::blocking::Client::new();
    let mut req = client.get(url);
    for (k, v) in headers {
        req = req.header(*k, *v);
    }
    let res = req.send().unwrap();
    (res.status().as_u16(), res.text().unwrap())
}

/// The script from `server`, run with `env`, installing into `home`.
fn install(server: &Server, home: &Path, env: &[(&str, &str)]) -> Output {
    let (status, script) = get(&format!("{}/install.sh", server.base), &[]);
    assert_eq!(status, 200);
    Command::new("sh")
        .arg("-c")
        .arg(script)
        .env("HOME", home)
        .envs(env.iter().copied())
        .output()
        .unwrap()
}

#[test]
fn installing_a_release() {
    let dir = tempfile::tempdir().unwrap();
    release(dir.path(), "0.1.0");
    release(dir.path(), "0.2.0");
    let server = serve(Some(dir.path()), None);
    let home = dir.path().join("home");

    let out = install(&server, &home, &[]);
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        out.status.success(),
        "{stdout}{}",
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(stdout.contains(&format!("installing 0.2.0 for {}", target())));
    // Into ~/.local/bin, which is not on this PATH.
    assert!(stdout.contains("is not on your PATH"));
    assert!(stdout.contains("knotless login"));
    for bin in BINS {
        let ran = Command::new(home.join(".local/bin").join(bin))
            .output()
            .unwrap();
        assert_eq!(String::from_utf8_lossy(&ran.stdout).trim(), bin);
    }

    // A version asked for, into a directory on the PATH, over what is there.
    let bin = dir.path().join("bin");
    let path = format!("{}:{}", bin.display(), std::env::var("PATH").unwrap());
    let to = bin.to_str().unwrap();
    for _ in 0..2 {
        let out = install(
            &server,
            &home,
            &[
                ("KNOTLESS_VERSION", "0.1.0"),
                ("KNOTLESS_INSTALL_DIR", to),
                ("PATH", &path),
            ],
        );
        let stdout = String::from_utf8_lossy(&out.stdout);
        assert!(out.status.success());
        assert!(stdout.contains("installing 0.1.0"));
        assert!(!stdout.contains("is not on your PATH"));
    }
    assert!(bin.join("knotless-mcp").exists());
    // A release from before the runner shipped installs without it.
    assert!(!bin.join("knotless-run").exists());

    // No such version.
    let out = install(&server, &home, &[("KNOTLESS_VERSION", "9.9.9")]);
    assert!(!out.status.success());
    assert!(String::from_utf8_lossy(&out.stderr).contains("no knotless-9.9.9-"));

    // An archive that is not what its checksum says.
    let name = format!("knotless-0.2.0-{}.tar.gz", target());
    let sum = dir
        .path()
        .join("releases/0.2.0")
        .join(format!("{name}.sha256"));
    std::fs::write(&sum, format!("{}  {name}\n", "0".repeat(64))).unwrap();
    let out = install(&server, &home, &[]);
    assert!(!out.status.success());
    assert!(String::from_utf8_lossy(&out.stderr).contains("does not match its checksum"));
    std::fs::remove_file(&sum).unwrap();
    let out = install(&server, &home, &[]);
    assert!(String::from_utf8_lossy(&out.stderr).contains("no checksum"));

    // No release at all.
    std::fs::remove_file(dir.path().join("releases/latest")).unwrap();
    let out = install(&server, &home, &[]);
    assert!(!out.status.success());
    assert!(String::from_utf8_lossy(&out.stderr).contains("no release at"));
}

#[test]
fn where_the_script_fetches_from() {
    let dir = tempfile::tempdir().unwrap();
    let server = serve(Some(dir.path()), None);
    let url = format!("{}/install.sh", server.base);
    let (_, script) = get(&url, &[]);
    assert!(script.contains(&format!("releases=\"{}/releases\"", server.base)));
    // Behind a proxy that holds the certificate.
    let (_, script) = get(
        &url,
        &[("x-forwarded-proto", "https"), ("host", "git.example")],
    );
    assert!(script.contains("releases=\"https://git.example/releases\""));
    assert!(script.contains("curl -fsSL https://git.example/install.sh | sh"));

    // The URL this server is reached at, when it is told it.
    let server = serve(Some(dir.path()), Some("https://knot.example/"));
    let (_, script) = get(&format!("{}/install.sh", server.base), &[]);
    assert!(script.contains("releases=\"https://knot.example/releases\""));

    // A server with no releases hands out no script.
    let server = serve(None, None);
    assert_eq!(get(&format!("{}/install.sh", server.base), &[]).0, 404);
}

#[test]
fn installing_a_published_release() {
    let mut s = Sandbox::new();
    let handle = s.account.handle.clone();
    s.set("KNOTLESS_IDENTIFIER", &handle);
    let url = s.url("knotless");
    let work = s.work("w", 1);
    s.git(&work, &["push", "-q", &url, "main"]);
    let staged = s.path("built");
    for version in ["0.1.0", "0.2.0-rc.1"] {
        let archive = archive(&staged, &staged.join(version), version);
        let file = archive.to_string_lossy();
        let (code, out) = s.cli(
            &["release", "publish", &url, "knotless", version, &file],
            b"",
        );
        assert_eq!(code, 0, "{out}");
    }
    let source = Source::parse(&url).unwrap();
    let server = serve_from(Some(source), None, ctx(&s));
    let releases = format!("{}/releases", server.base);

    // The latest is the highest that is not a pre-release.
    assert_eq!(
        get(&format!("{releases}/latest"), &[]),
        (200, "0.1.0\n".into())
    );
    let home = s.path("home");
    let out = install(&server, &home, &[]);
    let stdout = String::from_utf8_lossy(&out.stdout);
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(out.status.success(), "{stdout}{stderr}");
    assert!(stdout.contains("installing 0.1.0"));
    let ran = Command::new(home.join(".local/bin/knotless"))
        .output()
        .unwrap();
    assert_eq!(String::from_utf8_lossy(&ran.stdout).trim(), "knotless");

    // Kept once read, and handed out from there again.
    let name = format!("knotless-0.2.0-rc.1-{}.tar.gz", target());
    let bytes = std::fs::read(staged.join("0.2.0-rc.1").join(&name)).unwrap();
    let sum = format!("{:x}", Sha256::digest(&bytes));
    for _ in 0..2 {
        let res = reqwest::blocking::get(format!("{releases}/0.2.0-rc.1/{name}")).unwrap();
        assert_eq!(res.bytes().unwrap().as_ref(), bytes.as_slice());
    }
    let kept = ctx(&s).env.cache_dir.join("serve/releases");
    assert!(kept.join(&sum).exists());
    let checksum = get(&format!("{releases}/0.2.0-rc.1/{name}.sha256"), &[]);
    assert_eq!(checksum, (200, format!("{sum}  {name}\n")));

    // What is not there.
    for missing in ["9.9.9/x", "0.1.0/x", "0.1.0/x.sha256"] {
        assert_eq!(
            get(&format!("{releases}/{missing}"), &[]).0,
            404,
            "{missing}"
        );
    }

    // A file that cannot be kept is not handed out.
    let blocked = s.path("blocked").to_string_lossy().into_owned();
    s.set("KNOTLESS_CACHE_DIR", &blocked);
    std::fs::create_dir_all(s.path("blocked/serve")).unwrap();
    std::fs::write(s.path("blocked/serve/releases"), b"").unwrap();
    let server = serve_from(Some(Source::parse(&url).unwrap()), None, ctx(&s));
    let name = format!("knotless-0.1.0-{}.tar.gz", target());
    let res = get(&format!("{}/releases/0.1.0/{name}", server.base), &[]);
    assert_eq!(res.0, 502);

    // A repository with no releases, and one that is not there.
    let empty = s.url("empty");
    s.git(&work, &["push", "-q", &empty, "main"]);
    let server = serve_from(Some(Source::parse(&empty).unwrap()), None, ctx(&s));
    let (status, body) = get(&format!("{}/releases/latest", server.base), &[]);
    assert_eq!((status, body.as_str()), (404, "no release\n"));
    let gone = Source::parse(&s.url("gone")).unwrap();
    let server = serve_from(Some(gone), None, ctx(&s));
    for path in ["latest", "0.1.0/x"] {
        let (status, body) = get(&format!("{}/releases/{path}", server.base), &[]);
        assert_eq!(status, 502, "{path}");
        assert!(body.contains("no such repository"), "{body}");
    }
}

/// An OCI image layout in `dir`: one layer holding `says`, its config and
/// manifest, and the index naming it. Returns the manifest's bytes and
/// digest, and the layer's digest.
fn image(dir: &Path, says: &str) -> (Vec<u8>, String, String) {
    let digest = |b: &[u8]| format!("sha256:{:x}", Sha256::digest(b));
    let blobs = dir.join("blobs/sha256");
    std::fs::create_dir_all(&blobs).unwrap();
    let put = |b: &[u8]| {
        let d = digest(b);
        std::fs::write(blobs.join(&d[7..]), b).unwrap();
        d
    };
    let layer = put(says.as_bytes());
    let config = br#"{"architecture":"amd64","os":"linux"}"#;
    let config_digest = put(config);
    let manifest = serde_json::json!({
        "schemaVersion": 2,
        "mediaType": "application/vnd.oci.image.manifest.v1+json",
        "config": {"mediaType": "application/vnd.oci.image.config.v1+json", "digest": config_digest, "size": config.len()},
        "layers": [{"mediaType": "application/vnd.oci.image.layer.v1.tar", "digest": layer, "size": says.len()}],
    })
    .to_string()
    .into_bytes();
    let manifest_digest = put(&manifest);
    let index = serde_json::json!({"schemaVersion": 2, "manifests": [
        {"mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": manifest_digest, "size": manifest.len()}
    ]});
    std::fs::write(dir.join("index.json"), index.to_string()).unwrap();
    std::fs::write(dir.join("oci-layout"), r#"{"imageLayoutVersion":"1.0.0"}"#).unwrap();
    (manifest, manifest_digest, layer)
}

fn registry(ctx: knotless::env::Ctx) -> Server {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let listener = rt
        .block_on(tokio::net::TcpListener::bind("127.0.0.1:0"))
        .unwrap();
    let base = format!("http://{}", listener.local_addr().unwrap());
    let router = knotless_serve::oci::routes(ctx, None);
    rt.spawn(async move { axum::serve(listener, router).await.unwrap() });
    Server { base, _rt: rt }
}

#[test]
fn pulling_an_image() {
    let mut s = Sandbox::new();
    let handle = s.account.handle.clone();
    s.set("KNOTLESS_IDENTIFIER", &handle);
    let url = s.url("tools");
    let work = s.work("w", 1);
    s.git(&work, &["push", "-q", &url, "main"]);
    let (stable, rc) = (s.path("stable"), s.path("rc"));
    let (manifest, manifest_digest, layer) = image(&stable, "stable layer");
    let (_, rc_digest, _) = image(&rc, "rc layer");
    for (version, dir) in [("1.0.0", &stable), ("1.1.0-rc.1", &rc)] {
        let dir = dir.to_string_lossy();
        let (code, out) = s.cli(
            &["release", "publish", &url, "tools", version, "--oci", &dir],
            b"",
        );
        assert_eq!(code, 0, "{out}");
    }
    let server = registry(ctx(&s));
    let v2 = format!("{}/v2", server.base);
    let client = reqwest::blocking::Client::new();
    let fetch = |path: &str| client.get(format!("{v2}/{path}")).send().unwrap();

    // Asked first, it asks for a token; any, as a pull's is anyone's.
    let res = fetch("");
    assert_eq!(res.status(), 401);
    let asked = res.headers()["www-authenticate"]
        .to_str()
        .unwrap()
        .to_string();
    assert_eq!(
        asked,
        format!("Bearer realm=\"{v2}/token\",service=\"knotless\"")
    );
    let token: serde_json::Value = fetch("token").json().unwrap();
    assert_eq!(token["token"], "anonymous");
    let res = client
        .get(format!("{v2}/"))
        .bearer_auth("anonymous")
        .send()
        .unwrap();
    assert_eq!(
        res.headers()["docker-distribution-api-version"],
        "registry/2.0"
    );
    assert_eq!(res.text().unwrap(), "{}");
    // Its tags: the versions, and the latest.
    let tags: serde_json::Value = fetch(&format!("{handle}/tools/tags/list")).json().unwrap();
    let name = format!("{handle}/tools/tools");
    assert_eq!(
        tags,
        serde_json::json!({"name": name, "tags": ["1.1.0-rc.1", "1.0.0", "latest"]})
    );
    let none: serde_json::Value = fetch(&format!("{handle}/tools/other/tags/list"))
        .json()
        .unwrap();
    assert_eq!(none["tags"], serde_json::json!([]));

    // The latest is the highest that is not a pre-release, as its index names it.
    let res = fetch(&format!("{handle}/tools/manifests/latest"));
    assert_eq!(res.status(), 200);
    let headers = res.headers().clone();
    assert_eq!(
        headers["content-type"],
        "application/vnd.oci.image.manifest.v1+json"
    );
    assert_eq!(headers["docker-content-digest"], manifest_digest.as_str());
    assert_eq!(res.bytes().unwrap().as_ref(), manifest.as_slice());
    // By version, under the package's own name, and by digest.
    let res = fetch(&format!("{handle}/tools/tools/manifests/1.1.0-rc.1"));
    assert_eq!(res.headers()["docker-content-digest"], rc_digest.as_str());
    let res = fetch(&format!("{handle}/tools/manifests/{manifest_digest}"));
    assert_eq!(res.bytes().unwrap().as_ref(), manifest.as_slice());
    let head = client
        .head(format!("{v2}/{handle}/tools/manifests/latest"))
        .send()
        .unwrap();
    assert_eq!(head.status(), 200);
    assert_eq!(
        head.headers()["docker-content-digest"],
        manifest_digest.as_str()
    );
    // A layer, read once and kept.
    for _ in 0..2 {
        let res = fetch(&format!("{handle}/tools/blobs/{layer}"));
        assert_eq!(res.headers()["docker-content-digest"], layer.as_str());
        assert_eq!(res.text().unwrap(), "stable layer");
    }

    // What is not there, as the spec's errors.
    let code = |path: &str| {
        let res = fetch(path);
        let status = res.status().as_u16();
        let body: serde_json::Value = res.json().unwrap();
        (
            status,
            body["errors"][0]["code"].as_str().unwrap().to_string(),
        )
    };
    let none = format!("sha256:{}", "0".repeat(64));
    for (path, want) in [
        (
            format!("{handle}/tools/manifests/9.9.9"),
            "MANIFEST_UNKNOWN",
        ),
        (
            format!("{handle}/tools/manifests/{none}"),
            "MANIFEST_UNKNOWN",
        ),
        (format!("{handle}/tools/blobs/{none}"), "BLOB_UNKNOWN"),
        (
            format!("{handle}/tools/other/manifests/latest"),
            "MANIFEST_UNKNOWN",
        ),
        (format!("{handle}/manifests/latest"), "NAME_UNKNOWN"),
        (format!("{handle}/a%20b/manifests/latest"), "NAME_INVALID"),
        (format!("{handle}/gone/manifests/latest"), "NAME_UNKNOWN"),
        (
            format!("{handle}/tools/blobs/uploads/x"),
            "BLOB_UPLOAD_UNKNOWN",
        ),
    ] {
        assert_eq!(code(&path), (404, want.to_string()), "{path}");
    }

    // What cannot be kept is not handed out.
    let blocked = s.path("blocked").to_string_lossy().into_owned();
    s.set("KNOTLESS_CACHE_DIR", &blocked);
    std::fs::create_dir_all(s.path("blocked/serve")).unwrap();
    std::fs::write(s.path("blocked/serve/releases"), b"").unwrap();
    let server = registry(ctx(&s));
    let v2 = format!("{}/v2", server.base);
    for path in [
        format!("{handle}/tools/manifests/latest"),
        format!("{handle}/tools/manifests/{manifest_digest}"),
        format!("{handle}/tools/blobs/{layer}"),
    ] {
        let res = client.get(format!("{v2}/{path}")).send().unwrap();
        assert_eq!(res.status(), 502, "{path}");
    }
}

/// A push to `v2`, as a registry client makes one, with `token`.
#[derive(Clone)]
struct Pushing<'a> {
    v2: &'a str,
    name: String,
    token: &'a str,
    client: reqwest::blocking::Client,
}

impl Pushing<'_> {
    fn send(
        &self,
        method: reqwest::Method,
        path: &str,
        body: Vec<u8>,
    ) -> reqwest::blocking::Response {
        let url = format!("{}/{}{path}", self.v2, self.name);
        let req = self
            .client
            .request(method, url)
            .bearer_auth(self.token)
            .body(body);
        req.send().unwrap()
    }

    /// The error's status and code, of what was answered.
    fn code(res: reqwest::blocking::Response) -> (u16, String) {
        let status = res.status().as_u16();
        let body: serde_json::Value = res.json().unwrap();
        (
            status,
            body["errors"][0]["code"].as_str().unwrap().to_string(),
        )
    }

    /// Pushes `bytes` as one blob, in one request.
    fn blob(&self, bytes: &[u8]) -> String {
        let digest = format!("sha256:{:x}", Sha256::digest(bytes));
        let path = format!("/blobs/uploads/?digest={digest}");
        let res = self.send(reqwest::Method::POST, &path, bytes.to_vec());
        assert_eq!(res.status(), 201, "{:?}", res.text());
        digest
    }

    fn manifest(
        &self,
        reference: &str,
        manifest: &serde_json::Value,
    ) -> reqwest::blocking::Response {
        let url = format!("{}/{}/manifests/{reference}", self.v2, self.name);
        let req = self.client.put(url).bearer_auth(self.token);
        let req = req.header("content-type", "application/vnd.oci.image.manifest.v1+json");
        req.body(manifest.to_string()).send().unwrap()
    }
}

#[test]
fn pushing_an_image() {
    use reqwest::Method;
    let mut s = Sandbox::new();
    let handle = s.account.handle.clone();
    s.set("KNOTLESS_IDENTIFIER", &handle);
    let url = s.url("app");
    let work = s.work("w", 1);
    s.git(&work, &["push", "-q", &url, "main"]);
    let server = registry(ctx(&s));
    let v2 = format!("{}/v2", server.base);
    let client = reqwest::blocking::Client::new();

    // A token for the owner's app password, and none for a wrong one.
    let password = s.account.password.clone();
    let login = |v2: &str, password: &str| {
        let res = client
            .get(format!("{v2}/token"))
            .basic_auth(&handle, Some(password));
        res.send().unwrap()
    };
    assert_eq!(login(&v2, "wrong").status(), 401);
    let token: serde_json::Value = login(&v2, &password).json().unwrap();
    let token = token["token"].as_str().unwrap().to_string();
    assert_eq!(token.len(), 32);
    let res = client
        .get(format!("{v2}/"))
        .bearer_auth(&token)
        .send()
        .unwrap();
    assert_eq!(res.status(), 200);
    let p = Pushing {
        v2: &v2,
        name: format!("{handle}/app"),
        token: &token,
        client: client.clone(),
    };

    // A layer in parts: begun, gone on with, and ended.
    let layer: Vec<u8> = (0..3000u32).map(|i| (i % 251) as u8).collect();
    let layer_digest = format!("sha256:{:x}", Sha256::digest(&layer));
    let begun = p.send(Method::POST, "/blobs/uploads/", vec![]);
    assert_eq!(begun.status(), 202);
    let id = begun.headers()["docker-upload-uuid"]
        .to_str()
        .unwrap()
        .to_string();
    let at = begun.headers()["location"].to_str().unwrap();
    assert_eq!(at, format!("/v2/{handle}/app/app/blobs/uploads/{id}"));
    let going = p.send(
        Method::PATCH,
        &format!("/blobs/uploads/{id}"),
        layer[..1000].to_vec(),
    );
    assert_eq!(going.status(), 202);
    assert_eq!(going.headers()["range"], "0-999");
    // Ended with no digest, or the wrong one, it is refused, and gone.
    let end = format!("/blobs/uploads/{id}");
    let res = p.send(Method::PUT, &end, vec![]);
    assert_eq!(Pushing::code(res), (400, "DIGEST_INVALID".into()));
    let wrong = format!("{end}?digest=sha256:{}", "0".repeat(64));
    let res = p.send(Method::PUT, &wrong, layer[1000..].to_vec());
    assert_eq!(Pushing::code(res), (400, "DIGEST_INVALID".into()));
    let res = p.send(Method::PATCH, &end, vec![1]);
    assert_eq!(Pushing::code(res), (404, "BLOB_UPLOAD_UNKNOWN".into()));
    let res = p.send(Method::PUT, &wrong, vec![1]);
    assert_eq!(Pushing::code(res), (404, "BLOB_UPLOAD_UNKNOWN".into()));
    let begun = p.send(Method::POST, "/blobs/uploads/", vec![]);
    let id = begun.headers()["docker-upload-uuid"]
        .to_str()
        .unwrap()
        .to_string();
    p.send(
        Method::PATCH,
        &format!("/blobs/uploads/{id}"),
        layer[..1000].to_vec(),
    );
    let end = format!("/blobs/uploads/{id}?digest={layer_digest}");
    let res = p.send(Method::PUT, &end, layer[1000..].to_vec());
    assert_eq!(res.status(), 201);
    assert_eq!(
        res.headers()["docker-content-digest"],
        layer_digest.as_str()
    );
    // Pushed, it is there to read before any release holds it.
    let staged = format!("{v2}/{handle}/app/blobs/{layer_digest}");
    assert_eq!(client.head(&staged).send().unwrap().status(), 200);

    let config = p.blob(br#"{"architecture":"amd64","os":"linux"}"#);
    let image = serde_json::json!({
        "schemaVersion": 2,
        "mediaType": "application/vnd.oci.image.manifest.v1+json",
        "config": {"mediaType": "application/vnd.oci.image.config.v1+json", "digest": config, "size": 37},
        "layers": [{"mediaType": "application/vnd.oci.image.layer.v1.tar", "digest": layer_digest, "size": 3000}],
    });
    // The manifest by its digest, and an index naming it by a version.
    let image_digest = format!("sha256:{:x}", Sha256::digest(image.to_string()));
    let res = p.manifest(&format!("sha256:{}", "0".repeat(64)), &image);
    assert_eq!(Pushing::code(res), (400, "DIGEST_INVALID".into()));
    assert_eq!(p.manifest(&image_digest, &image).status(), 201);
    let index = serde_json::json!({
        "schemaVersion": 2,
        "mediaType": "application/vnd.oci.image.index.v1+json",
        "manifests": [{"mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": image_digest, "size": 1}],
    });
    // Tags are versions; `latest` is computed.
    let res = p.manifest("latest", &index);
    assert_eq!(Pushing::code(res), (400, "TAG_INVALID".into()));
    let res = p.manifest("1.0.0", &index);
    assert_eq!(res.status(), 201, "{:?}", res.text());
    let res = p.manifest("1.0.0", &index);
    assert_eq!(Pushing::code(res), (403, "DENIED".into()));

    // Pulled as any image published as a release is.
    let fetch = |path: &str| {
        let res = client.get(format!("{v2}/{handle}/app/{path}"));
        res.send().unwrap()
    };
    let tags: serde_json::Value = fetch("tags/list").json().unwrap();
    assert_eq!(tags["tags"], serde_json::json!(["1.0.0", "latest"]));
    let top = fetch("manifests/latest");
    let index_type = "application/vnd.oci.image.index.v1+json";
    assert_eq!(top.headers()["content-type"], index_type);
    let child = fetch(&format!("manifests/{image_digest}"));
    assert_eq!(child.text().unwrap(), image.to_string());
    let pulled = fetch(&format!("blobs/{layer_digest}")).bytes().unwrap();
    assert_eq!(pulled.as_ref(), layer.as_slice());
    let (code, out) = s.cli(&["release", "list", &url], b"");
    assert_eq!(code, 0, "{out}");
    assert!(out.starts_with("app\t1.0.0\t"), "{out}");

    // What a manifest reaches must be pushed first.
    let config = format!("sha256:{}", "1".repeat(64));
    let missing = serde_json::json!({"config": {"digest": config, "size": 1}, "layers": []});
    let res = p.manifest("2.0.0", &missing);
    assert_eq!(Pushing::code(res), (400, "MANIFEST_BLOB_UNKNOWN".into()));
    let child = format!("sha256:{}", "2".repeat(64));
    let orphan = serde_json::json!({"manifests": [{"digest": child, "size": 1}]});
    let res = p.manifest("2.0.0", &orphan);
    assert_eq!(Pushing::code(res), (400, "MANIFEST_BLOB_UNKNOWN".into()));
    // Nor is what is not a manifest taken.
    let res = p.send(Method::PUT, "/manifests/2.0.0", b"not json".to_vec());
    assert_eq!(Pushing::code(res), (400, "MANIFEST_INVALID".into()));
    let res = p.send(Method::PUT, "/manifests/2.0.0", vec![b' '; (4 << 20) + 1]);
    assert_eq!(Pushing::code(res), (400, "MANIFEST_INVALID".into()));

    // Nothing else is posted, patched or put.
    let blob = format!("/blobs/{layer_digest}");
    for (method, path) in [
        (Method::POST, "/manifests/1.0.0"),
        (Method::PATCH, "/manifests/1.0.0"),
        (Method::PUT, blob.as_str()),
    ] {
        let res = p.send(method, path, vec![]);
        assert_eq!(Pushing::code(res), (405, "UNSUPPORTED".into()));
    }
    let res = client.post(format!("{v2}/nowhere")).send().unwrap();
    assert_eq!(Pushing::code(res), (405, "UNSUPPORTED".into()));

    // Only the owner pushes, with a token, to a repository there can be.
    let uploads = format!("{v2}/{handle}/app/blobs/uploads/");
    let res = client.post(uploads).send().unwrap();
    assert_eq!(res.status(), 401);
    let asked = res.headers()["www-authenticate"].to_str().unwrap();
    let scope = format!(",scope=\"repository:{handle}/app/app:push,pull\"");
    assert!(asked.ends_with(&scope), "{asked}");
    let other = Sandbox::new();
    for (name, status, code) in [
        (format!("{}/app", other.account.handle), 403, "DENIED"),
        ("nobody.invalid/app".to_string(), 404, "NAME_UNKNOWN"),
        (format!("{handle}/a%20b"), 404, "NAME_INVALID"),
    ] {
        let p = Pushing { name, ..p.clone() };
        let res = p.send(Method::POST, "/blobs/uploads/", vec![]);
        assert_eq!(Pushing::code(res), (status, code.to_string()));
    }

    // A push whose blobs cannot be kept fails.
    let blocked = s.path("blocked").to_string_lossy().into_owned();
    s.set("KNOTLESS_CACHE_DIR", &blocked);
    std::fs::create_dir_all(s.path("blocked/serve")).unwrap();
    std::fs::write(s.path("blocked/serve/uploads"), b"").unwrap();
    let server = registry(ctx(&s));
    let v2 = format!("{}/v2", server.base);
    let token: serde_json::Value = login(&v2, &password).json().unwrap();
    let p = Pushing {
        v2: &v2,
        name: format!("{handle}/app"),
        token: token["token"].as_str().unwrap(),
        client: client.clone(),
    };
    let res = p.send(Method::POST, "/blobs/uploads/", vec![]);
    assert_eq!(Pushing::code(res).0, 502);
    assert_eq!(Pushing::code(p.manifest(&image_digest, &image)).0, 502);
}

// A lost race is told apart from any other failed write, which says what
// went wrong rather than that the version was published.
#[test]
fn a_release_not_made_says_why() {
    use knotless::push;
    use knotless::records::{RELEASE_MIME, ReleaseFile, ReleasePart};
    use knotless::release::{Made, Target};
    use knotless::url::RepoUrl;
    use knotless::xrpc::Auth;

    let mut s = Sandbox::new();
    let handle = s.account.handle.clone();
    s.set("KNOTLESS_IDENTIFIER", &handle);
    let url = s.url("raced");
    let work = s.work("w", 1);
    s.git(&work, &["push", "-q", &url, "main"]);
    let ctx = ctx(&s);
    let url = RepoUrl::parse(&url).unwrap();
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        let session = knotless::session::create(&ctx, &handle, &s.account.password);
        let auth = session.await.unwrap().auth();
        let target = Target::open(&ctx, &url, "tool", "1.0.0").await.unwrap();
        let blobs = push::upload(&ctx, &target.remote, &auth, b"x", RELEASE_MIME);
        let file = ReleaseFile {
            name: "x".into(),
            size: 1,
            sha256: knotless::release::sha256(b"x"),
            parts: blobs
                .await
                .unwrap()
                .into_iter()
                .map(|blob| ReleasePart { blob })
                .collect(),
        };
        let stranger = Auth::Bearer("no such token".into());
        let create = |auth| target.create(auth, vec![file.clone()], Made::default());

        let refused = create(&stranger).await.unwrap_err();
        assert!(
            !format!("{refused:#}").contains("published already"),
            "{refused:#}"
        );

        create(&auth).await.unwrap();
        let lost = create(&auth).await.unwrap_err();
        assert!(
            format!("{lost:#}").contains("tool 1.0.0 is published already"),
            "{lost:#}"
        );
    });
}

/// An appview handing out the releases in `dir`, whose describeServer
/// answers what `described` holds.
fn appview(dir: &Path, described: Arc<Mutex<serde_json::Value>>) -> Server {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let listener = rt
        .block_on(tokio::net::TcpListener::bind("127.0.0.1:0"))
        .unwrap();
    let base = format!("http://{}", listener.local_addr().unwrap());
    let ctx = knotless::env::Ctx::new(knotless::env::Env::from_vars(|_| None));
    let source = Some(Source::Dir(dir.join("releases")));
    let describe = move || {
        let described = described.lock().unwrap().clone();
        async move { axum::Json(described) }
    };
    let router = knotless_serve::releases::routes(source, None, ctx).route(
        "/xrpc/dev.knotless.describeServer",
        axum::routing::get(describe),
    );
    rt.spawn(async move { axum::serve(listener, router).await.unwrap() });
    Server { base, _rt: rt }
}

#[test]
fn a_newer_version_is_told_of_and_an_older_one_refused() {
    let dir = tempfile::tempdir().unwrap();
    release(dir.path(), "999.0.0");
    let described = Arc::new(Mutex::new(serde_json::json!({"runners": []})));
    let server = appview(dir.path(), described.clone());
    let mut s = Sandbox::new();
    s.set("KNOTLESS_APPVIEW", &server.base);
    s.set("KNOTLESS_NO_UPDATE_CHECK", "");

    // Told on stderr that a newer one is out, and asked once a day.
    let (ok, out, err) = s.knotless(&["help"], b"");
    assert!(ok && out.contains("usage:"));
    assert!(err.contains("knotless 999.0.0 is out"), "{err}");
    let kept = s.path("cache/update.json");
    assert!(kept.exists());
    *described.lock().unwrap() = serde_json::json!({
        "runners": [],
        "client": {"minimum": "999.0.0", "reason": "Compaction loses objects."},
    });
    // A day is not up, so the floor is not known yet.
    let refused = |s: &Sandbox| s.knotless(&["nope"], b"").2.contains("asks for");
    assert!(!refused(&s));

    // A day on, the appview asks for 999.0.0, and nothing but what gets
    // there runs.
    let stale = || {
        let mut known: serde_json::Value =
            serde_json::from_slice(&std::fs::read(&kept).unwrap()).unwrap();
        known["checked"] = 0.into();
        std::fs::write(&kept, known.to_string()).unwrap();
    };
    stale();
    let (ok, _, err) = s.knotless(&["nope"], b"");
    assert!(!ok);
    assert!(
        err.contains("older than the 999.0.0 the appview asks for. Compaction loses objects."),
        "{err}"
    );
    assert!(!err.contains("is out"), "{err}");
    assert_eq!(s.cli(&["help"], b"").0, 0);
    // Unless told to go on anyway.
    s.set("KNOTLESS_IGNORE_MINIMUM", "1");
    let (ok, _, err) = s.knotless(&["nope"], b"");
    assert!(!ok && err.contains("unknown command nope"), "{err}");
    s.set("KNOTLESS_IGNORE_MINIMUM", "");

    // An appview that cannot be asked leaves what was known.
    stale();
    s.set("KNOTLESS_APPVIEW", "http://127.0.0.1:9");
    assert!(refused(&s));
    // Nor is there anything to know with checks off.
    s.set("KNOTLESS_NO_UPDATE_CHECK", "1");
    assert!(!refused(&s));
    s.set("KNOTLESS_NO_UPDATE_CHECK", "");
    s.set("KNOTLESS_APPVIEW", &server.base);

    // Upgraded, from the appview's script, into the directory asked for.
    let bin = dir.path().join("bin");
    s.set("KNOTLESS_INSTALL_DIR", &bin.to_string_lossy());
    let (ok, out, err) = s.knotless(&["upgrade"], b"");
    assert!(ok, "{err}");
    assert!(out.starts_with("knotless: installing 999.0.0"), "{out}");
    assert!(!err.contains("is out"), "{err}");
    let ran = Command::new(bin.join("knotless")).output().unwrap();
    assert_eq!(String::from_utf8_lossy(&ran.stdout).trim(), "knotless");
    // Or beside the binary that runs, when none is.
    s.set("KNOTLESS_INSTALL_DIR", "");
    let copied = dir.path().join("copied");
    std::fs::create_dir_all(&copied).unwrap();
    std::fs::copy(bin_dir().join("knotless"), copied.join("knotless")).unwrap();
    let out = s
        .command(&copied.join("knotless").to_string_lossy(), s.dir.path())
        .arg("upgrade")
        .output()
        .unwrap();
    assert!(
        out.status.success(),
        "{}",
        String::from_utf8_lossy(&out.stderr)
    );
    let ran = Command::new(copied.join("knotless")).output().unwrap();
    assert_eq!(String::from_utf8_lossy(&ran.stdout).trim(), "knotless");

    // What cannot be upgraded so.
    let upgrade = |s: &Sandbox| {
        let (ok, _, err) = s.knotless(&["upgrade"], b"");
        assert!(!ok);
        err
    };
    s.set("KNOTLESS_INSTALL_DIR", "/home/a/.cargo/bin");
    assert!(upgrade(&s).contains("upgrade it with cargo"));
    s.set("KNOTLESS_INSTALL_DIR", &bin.to_string_lossy());
    s.set("KNOTLESS_VERSION", "9.9.9");
    assert!(upgrade(&s).contains("no knotless-9.9.9-"));
    s.set("KNOTLESS_VERSION", "");
    let none = serve(None, None);
    s.set("KNOTLESS_APPVIEW", &none.base);
    assert!(upgrade(&s).contains("hands out no releases"));
    s.set("KNOTLESS_APPVIEW", "");
    assert!(upgrade(&s).contains("set KNOTLESS_APPVIEW"));
}