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

nordisk/gunnar

crates/gunnar-forge/tests/common/mod.rs

//! **Pages built from one REAL repository object per forge.**
//!
//! `tests/fixtures/*.json` are the response objects out of each API's own
//! documentation, whole — 100 keys for GitHub, 60 for Gitea, 80 for GitLab —
//! and these helpers clone one and override the four keys a test actually
//! varies (name, private, archived).
//!
//! # Why not hand-written three-key objects
//!
//! Because the bug that shape cannot find is the one that will happen: a forge
//! object where `description` is `null`, `mirror_url` is `git:` rather than
//! `https:`, `language` is `null`, `parent` is `null`, and `permissions` is a
//! nested object. A parser written against `{"full_name":…,"clone_url":…}`
//! passes its own tests and then meets `"language": null` in production. The
//! fixtures carry all of it, so the parser is exercised against the real
//! surface every time.
//!
//! # Why override rather than keep a file per case
//!
//! A pagination test needs 3 pages × N repositories, and an archived case and a
//! private case differ from the base by one boolean each. Twelve fixture files
//! that differ in one key would drift the day GitHub adds a field: eleven of
//! them would keep the old shape and nothing would say so. One file plus an
//! override keeps the real shape in exactly one place (one implementation).

// Each test binary in `tests/` includes this module and uses a subset of it —
// `token_never_leaks.rs` needs two of the builders, `list.rs` needs all of
// them. Rust compiles the module once per test binary, so the unused half is a
// warning in each. Allowed here rather than split into two modules, because two
// fixture modules is exactly the duplication the module docs argue against.
#![allow(dead_code)]

use std::path::PathBuf;

use serde_json::{json, Value};

/// A repository a test wants on a page.
#[derive(Clone, Copy)]
pub struct Row {
    pub name: &'static str,
    pub private: bool,
    pub archived: bool,
}

impl Row {
    pub fn public(name: &'static str) -> Row {
        Row {
            name,
            private: false,
            archived: false,
        }
    }
    pub fn private(name: &'static str) -> Row {
        Row {
            name,
            private: true,
            archived: false,
        }
    }
    pub fn archived(name: &'static str) -> Row {
        Row {
            name,
            private: false,
            archived: true,
        }
    }
}

fn fixture(file: &str) -> Value {
    let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures")
        .join(file);
    let raw = std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {p:?}: {e}"));
    serde_json::from_str(&raw).unwrap_or_else(|e| panic!("parse {p:?}: {e}"))
}

/// A page of GitHub repositories, as `GET /user/repos` returns it.
pub fn github_page(rows: &[Row]) -> String {
    let base = fixture("github_repo.json");
    let items: Vec<Value> = rows
        .iter()
        .map(|r| {
            let mut o = base.clone();
            let m = o.as_object_mut().unwrap();
            m.insert("name".into(), json!(r.name));
            m.insert("full_name".into(), json!(format!("octocat/{}", r.name)));
            m.insert(
                "clone_url".into(),
                json!(format!("https://github.com/octocat/{}.git", r.name)),
            );
            m.insert(
                "ssh_url".into(),
                json!(format!("git@github.com:octocat/{}.git", r.name)),
            );
            m.insert("private".into(), json!(r.private));
            m.insert("archived".into(), json!(r.archived));
            m.insert(
                "visibility".into(),
                json!(if r.private { "private" } else { "public" }),
            );
            o
        })
        .collect();
    Value::Array(items).to_string()
}

/// A page of Forgejo/Gitea repositories, as `GET /api/v1/user/repos` returns it.
///
/// The host is a parameter because the self-hosted test points a `Forge` at
/// `git.example.com` and then asserts the clone URLs came back with THAT host —
/// which is the property that would silently break if the driver ever
/// reconstructed URLs from its own base instead of reading the forge's.
pub fn gitea_page(host: &str, rows: &[Row]) -> String {
    let base = fixture("gitea_repo.json");
    let items: Vec<Value> = rows
        .iter()
        .map(|r| {
            let mut o = base.clone();
            let m = o.as_object_mut().unwrap();
            m.insert("name".into(), json!(r.name));
            m.insert("full_name".into(), json!(format!("alice/{}", r.name)));
            m.insert(
                "clone_url".into(),
                json!(format!("https://{host}/alice/{}.git", r.name)),
            );
            m.insert(
                "ssh_url".into(),
                json!(format!("git@{host}:alice/{}.git", r.name)),
            );
            m.insert("private".into(), json!(r.private));
            m.insert("archived".into(), json!(r.archived));
            o
        })
        .collect();
    Value::Array(items).to_string()
}

/// A page of GitLab projects, as `GET /api/v4/projects` returns it.
pub fn gitlab_page(rows: &[Row]) -> String {
    let base = fixture("gitlab_project.json");
    let items: Vec<Value> = rows
        .iter()
        .map(|r| {
            let mut o = base.clone();
            let m = o.as_object_mut().unwrap();
            m.insert("path".into(), json!(r.name));
            m.insert(
                "path_with_namespace".into(),
                json!(format!("diaspora/{}", r.name)),
            );
            m.insert(
                "http_url_to_repo".into(),
                json!(format!(
                    "https://gitlab.example.com/diaspora/{}.git",
                    r.name
                )),
            );
            m.insert(
                "ssh_url_to_repo".into(),
                json!(format!("git@gitlab.example.com:diaspora/{}.git", r.name)),
            );
            // GitLab has no `private` boolean. `internal` is used for the
            // private case on purpose: it is the visibility a naive
            // `visibility == "private"` test would report as public, which is
            // the bug worth pinning.
            m.insert(
                "visibility".into(),
                json!(if r.private { "internal" } else { "public" }),
            );
            m.insert("archived".into(), json!(r.archived));
            o
        })
        .collect();
    Value::Array(items).to_string()
}

/// The `Link` header GitHub sends on a page that has a successor.
pub fn link_next(base: &str, next: u32) -> String {
    format!(
        "<{base}/user/repos?page=1>; rel=\"first\", <{base}/user/repos?page={next}>; rel=\"next\""
    )
}