crates/gunnar-server/tests/new_repo_account_admin.rs
//! **Who may press "New repository"** (owner's decision, 2026-10-06): an
//! ACCOUNT ADMIN creates a repository in their own account and in a clan they
//! administer; whoever holds only write or read on a namespace does not, nor
//! does anybody in another account's namespace. gunnar decides — the console's
//! button is a relay of `Repos.Create` signed as the person — by the rules a
//! push-to-create meets: the account's name filter, only the account admin
//! may make it public, and the account's ROOM: a namespace its plan refuses
//! writes to, or already at its cap, gets no new repository either.
use std::sync::Arc;
use gunnar_grpc::{
Caller, ClansService, ControlPlane, EventBus, ReposService, ServerConfig, ServiceContext,
};
use gunnar_proto::clans_server::Clans as _;
use gunnar_proto::repos_server::Repos as _;
use gunnar_proto::*;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
use tonic::{Code, Request};
fn person(id: &str) -> Caller {
Caller {
principal_id: id.to_owned(),
fingerprint: format!("SHA256:{id}"),
machine: false,
authenticated: true,
}
}
fn authed<T>(caller: &Caller, body: T) -> Request<T> {
let mut r = Request::new(body);
r.extensions_mut().insert(caller.clone());
r
}
/// The estate: `op` the operator (server Admin); `acme` a customer and so the
/// admin of its own account; `wendy` holding WRITE on acme and `vic` holding
/// READ on it; `carol` the admin of the clan `fellows`, which `dave` joined.
struct World {
_dir: tempfile::TempDir,
stores: Arc<ServerStores>,
ctx: ServiceContext,
}
fn world() -> World {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
let control: Arc<dyn ControlPlane> = plane.clone();
let ctx = ServiceContext::new(
control,
Arc::new(EventBus::new(64)),
ServerConfig::default(),
);
plane
.add_principal("op", "op", PrincipalKind::User)
.expect("the operator");
plane
.grant("op", "", "", GrantLevel::Admin)
.expect("the operator is a server admin");
for id in ["acme", "wendy", "vic", "carol", "dave"] {
plane
.add_principal(id, id, PrincipalKind::User)
.expect("an account");
}
plane
.grant("wendy", "acme", "", GrantLevel::Write)
.expect("wendy writes acme");
plane
.grant("vic", "acme", "", GrantLevel::Read)
.expect("vic reads acme");
World {
_dir: dir,
stores,
ctx,
}
}
impl World {
fn repos(&self) -> ReposService {
ReposService::new(self.ctx.clone())
}
fn clans(&self) -> ClansService {
ClansService::new(self.ctx.clone())
}
async fn create(
&self,
who: &str,
ns: &str,
repo: &str,
visibility: Visibility,
) -> Result<Repo, tonic::Status> {
self.repos()
.create(authed(
&person(who),
CreateRepoRequest {
path: Some(RepoPath::new(ns, repo)),
visibility: visibility as i32,
..Default::default()
},
))
.await
.map(|r| r.into_inner().repo.expect("a repo"))
}
async fn fellows(&self) {
self.clans()
.create(authed(
&person("carol"),
CreateClanRequest {
name: "fellows".into(),
contribution_bytes: 0,
},
))
.await
.expect("carol makes a clan");
self.clans()
.invite(authed(
&person("carol"),
InviteToClanRequest {
clan: "fellows".into(),
principal_id: "dave".into(),
},
))
.await
.expect("carol invites dave");
self.clans()
.accept(authed(
&person("dave"),
AcceptClanInviteRequest {
clan: "fellows".into(),
contribution_bytes: 0,
},
))
.await
.expect("dave joins");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn the_account_admin_creates_in_their_own_account_private_and_public() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
w.create("acme", "acme", "site", Visibility::Private)
.await
.expect("the account admin, private");
let public = w
.create("acme", "acme", "www", Visibility::Public)
.await
.expect("the account admin, public");
assert_eq!(public.visibility, Visibility::Public as i32);
}
/// ★ **`Repos.Create{readme}` writes the first commit server-side** (F3, the owner's welcome
/// repository, 2026-10-08): the README on the default branch, in the repository's store, with
/// no git client; an empty `readme` creates an empty repository as before.
#[tokio::test(flavor = "multi_thread")]
async fn a_repository_created_with_a_readme_holds_it_as_its_first_commit() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
let readme = "# Welcome\n\nThank you for buying your private Gunnar.\n";
w.repos()
.create(authed(
&person("acme"),
CreateRepoRequest {
path: Some(RepoPath::new("acme", "welcome")),
visibility: Visibility::Private as i32,
readme: readme.into(),
..Default::default()
},
))
.await
.expect("the welcome repository");
let plane = CoreControlPlane::new(Arc::clone(&w.stores));
let path = RepoPath::new("acme", "welcome");
let main = plane
.get_ref(&path, "refs/heads/main", false)
.expect("the default branch holds a commit");
let read = plane
.read_object(&path, &main.oid, 1 << 20)
.expect("the commit reads back");
assert!(
String::from_utf8_lossy(&read.data).contains("author Gunnar <noreply@gunnar.invalid>"),
"{:?}",
String::from_utf8_lossy(&read.data)
);
let empty = w
.create("acme", "acme", "empty", Visibility::Private)
.await
.expect("no readme");
assert!(
plane
.get_ref(&RepoPath::new("acme", "empty"), "refs/heads/main", false)
.is_err(),
"no readme, no commit: {empty:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_writer_and_a_reader_of_the_account_create_nothing_in_it() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
for who in ["wendy", "vic"] {
for visibility in [Visibility::Private, Visibility::Public] {
let err = w
.create(who, "acme", &format!("by-{who}"), visibility)
.await
.expect_err("not the account's admin");
assert_eq!(
err.code(),
Code::PermissionDenied,
"{who} {visibility:?}: {err:?}"
);
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn nobody_creates_in_another_accounts_namespace() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
for (who, ns) in [("acme", "carol"), ("carol", "acme"), ("wendy", "vic")] {
let err = w
.create(who, ns, "intruder", Visibility::Private)
.await
.expect_err("another account's namespace");
assert_eq!(err.code(), Code::PermissionDenied, "{who} in {ns}: {err:?}");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn the_clan_admin_creates_in_the_clan_and_only_the_admin_makes_it_public() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
w.fellows().await;
w.create("carol", "fellows", "plans", Visibility::Private)
.await
.expect("the clan's admin, private");
w.create("carol", "fellows", "site", Visibility::Public)
.await
.expect("the clan's admin, public");
let err = w
.create("dave", "fellows", "shout", Visibility::Public)
.await
.expect_err("a member does not publish");
assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
let err = w
.create("acme", "fellows", "intruder", Visibility::Private)
.await
.expect_err("not in the clan");
assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
}
#[tokio::test(flavor = "multi_thread")]
async fn a_name_the_account_filter_stops_is_refused_at_create() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
let err = w
.create("acme", "acme", "fuck-this", Visibility::Private)
.await
.expect_err("the name filter");
assert_eq!(err.code(), Code::InvalidArgument, "{err:?}");
}
const SUSPENDED: &str =
"plan team is suspended: writes are refused until it is paid at https://gunnar.rs/billing/acme";
const FULL: &str =
"plan team holds 10 GiB of its 10 GiB: no room for more at https://gunnar.rs/billing/carol";
/// The account's plan, as the front doors ask it: `acme` suspended, `carol`
/// at its cap, everybody else free.
struct Plans;
impl gunnar_ssh::PlanGate for Plans {
fn name(&self) -> &'static str {
"plans-for-the-test"
}
fn verdict(&self, namespace: &str, _: gunnar_ssh::PlanAction) -> gunnar_ssh::PlanVerdict {
if namespace == "acme" {
gunnar_ssh::PlanVerdict::Refuse(SUSPENDED.to_owned())
} else {
gunnar_ssh::PlanVerdict::Allow
}
}
fn pack_quota(&self, namespace: &str) -> gunnar_ssh::PlanVerdict {
if namespace == "carol" {
gunnar_ssh::PlanVerdict::Refuse(FULL.to_owned())
} else {
gunnar_ssh::PlanVerdict::Allow
}
}
}
/// ★ **The account's room, as a push-to-create meets it**: a namespace its
/// plan refuses writes to, or one already at its cap, gets no new repository —
/// refused with the plan's own line, and nothing is made.
#[tokio::test(flavor = "multi_thread")]
async fn an_account_its_plan_refuses_or_at_its_cap_gets_no_new_repository() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
w.stores
.install_plan_gates(vec![Arc::new(Plans) as Arc<dyn gunnar_ssh::PlanGate>]);
for (who, words, code) in [
("acme", SUSPENDED, Code::FailedPrecondition),
("carol", FULL, Code::ResourceExhausted),
] {
let err = w
.create(who, who, "more", Visibility::Private)
.await
.expect_err("the plan says no");
assert_eq!(err.code(), code, "{who}: {err:?}");
assert!(
err.message().contains(words),
"{who}: the plan's own line: {err:?}"
);
let listed = w
.repos()
.list(authed(
&person(who),
ListReposRequest {
namespace: who.into(),
..Default::default()
},
))
.await
.expect("a listing")
.into_inner()
.repos;
assert!(listed.is_empty(), "{who}: a refused create left {listed:?}");
}
w.create("dave", "dave", "fine", Visibility::Private)
.await
.expect("an account the plan allows");
}