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\""
)
}