michal/tit
Browse tree · Show commit · Download archive
Blob: tests/cli.rs
mod support;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use sha2::{Digest, Sha256};
use support::{
TestInstance, create_bare_git_fixture, create_ssh_key_fixture, free_address,
read_stock_ssh_configuration,
};
use tempfile::TempDir;
const CURRENT_DATABASE: &str = concat!(
include_str!("fixtures/sqlite/v5.sql"),
include_str!("../src/store/migrations/006_repository_events.sql"),
include_str!("../src/store/migrations/007_account_lifecycle.sql"),
include_str!("../src/store/migrations/008_web_sessions.sql"),
include_str!("../src/store/migrations/009_repository_authorization.sql"),
include_str!("../src/store/migrations/010_audit_history.sql"),
include_str!("../src/store/migrations/011_domain_events.sql"),
include_str!("../src/store/migrations/012_issues.sql"),
include_str!("../src/store/migrations/013_watches.sql"),
include_str!("../src/store/migrations/014_feed_tokens.sql"),
include_str!("../src/store/migrations/015_pull_requests.sql"),
include_str!("../src/store/migrations/016_pull_request_reviews.sql"),
include_str!("../src/store/migrations/017_pull_request_merges.sql"),
include_str!("../src/store/migrations/018_streamlined_login.sql"),
include_str!("../src/store/migrations/019_product_reduction.sql"),
include_str!("../src/store/migrations/020_repository_profiles.sql"),
include_str!("../src/store/migrations/021_pull_request_lifecycle.sql"),
include_str!("../src/store/migrations/022_account_key_management.sql"),
include_str!("../src/store/migrations/023_default_branch.sql"),
"PRAGMA user_version = 23;\n",
);
#[test]
fn help_and_version_use_standard_output() {
for argument in ["--help", "--version"] {
let output = Command::new(env!("CARGO_BIN_EXE_tit"))
.arg(argument)
.output()
.expect("run tit");
assert!(output.status.success());
assert!(!output.stdout.is_empty());
assert!(output.stderr.is_empty());
}
}
#[test]
fn a_cli_error_uses_exit_code_two_and_standard_error() {
let output = Command::new(env!("CARGO_BIN_EXE_tit"))
.arg("--unknown")
.output()
.expect("run tit");
assert_eq!(output.status.code(), Some(2));
assert!(output.stdout.is_empty());
assert!(!output.stderr.is_empty());
}
#[test]
fn validates_configuration_without_output() {
let instance = TestInstance::new();
let config = instance.config().to_str().expect("a UTF-8 path");
let output = instance.run(&["--config", config]);
assert!(output.status.success());
assert!(output.stdout.is_empty());
assert!(output.stderr.is_empty());
}
#[test]
fn a_configuration_error_uses_exit_code_one_and_standard_error() {
let instance = TestInstance::new();
let missing = instance.path().join("missing.toml");
let output = instance.run(&["--config", missing.to_str().expect("a UTF-8 path")]);
assert_eq!(output.status.code(), Some(1));
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).starts_with("tit: "));
}
#[test]
fn doctor_checks_an_existing_current_database() {
let instance = TestInstance::new();
let database = rusqlite::Connection::open(instance.path().join("tit.sqlite3"))
.expect("open the instance database");
database
.execute_batch(CURRENT_DATABASE)
.expect("create the current database");
drop(database);
prepare_doctor_files(&instance);
let output = instance.run(&[
"--config",
instance.config().to_str().expect("a UTF-8 path"),
"doctor",
]);
assert!(
output.status.success(),
"doctor failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stdout.is_empty());
assert!(output.stderr.is_empty());
}
#[test]
fn doctor_does_not_create_or_migrate_a_database() {
let instance = TestInstance::new();
let database_path = instance.path().join("tit.sqlite3");
let arguments = [
"--config",
instance.config().to_str().expect("a UTF-8 path"),
"doctor",
];
let missing = instance.run(&arguments);
assert_eq!(missing.status.code(), Some(1));
assert!(!database_path.exists());
let database = rusqlite::Connection::open(&database_path).expect("open the old database");
database
.execute_batch(include_str!("fixtures/sqlite/v1.sql"))
.expect("create the old database");
drop(database);
let old = instance.run(&arguments);
assert_eq!(old.status.code(), Some(1));
let database = rusqlite::Connection::open(&database_path).expect("reopen the old database");
assert_eq!(
database
.pragma_query_value::<i64, _>(None, "user_version", |row| row.get(0))
.expect("read the schema version"),
1
);
}
#[test]
fn doctor_reports_a_foreign_key_violation() {
let instance = TestInstance::new();
let database = rusqlite::Connection::open(instance.path().join("tit.sqlite3"))
.expect("open the instance database");
database
.execute_batch(CURRENT_DATABASE)
.expect("create the current database");
database
.pragma_update(None, "foreign_keys", false)
.expect("disable foreign keys for the damaged fixture");
database
.execute(
"INSERT INTO m1a_child (id, parent_id, sequence, body) VALUES (2, 999, 1, 'orphan')",
[],
)
.expect("create a foreign-key violation");
drop(database);
prepare_doctor_files(&instance);
let output = instance.run(&[
"--config",
instance.config().to_str().expect("a UTF-8 path"),
"doctor",
]);
assert_eq!(output.status.code(), Some(1));
assert!(output.stdout.is_empty());
assert!(
String::from_utf8_lossy(&output.stderr).contains("foreign key violation"),
"unexpected doctor error: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn prepare_doctor_files(instance: &TestInstance) {
fs::set_permissions(
instance.path().join("tit.sqlite3"),
fs::Permissions::from_mode(0o600),
)
.expect("make the database private");
let repositories = instance.path().join("repositories");
fs::create_dir(&repositories).expect("create the repository directory");
fs::set_permissions(&repositories, fs::Permissions::from_mode(0o700))
.expect("make the repository directory private");
create_ssh_key_fixture(&instance.path().join("ssh_host_ed25519_key"));
}
#[test]
fn starts_without_git_or_openssh_on_the_path() {
let instance = TestInstance::new();
let config = instance.config().to_str().expect("a UTF-8 path");
let output = Command::new(env!("CARGO_BIN_EXE_tit"))
.args(["--config", config])
.env("PATH", "")
.output()
.expect("run tit");
assert!(output.status.success());
}
#[test]
fn reads_user_configuration_from_the_xdg_data_directory() {
let data = TempDir::new().expect("create a temporary data directory");
let instance = data.path().join("tit");
fs::create_dir(&instance).expect("create the instance directory");
fs::write(
instance.join("config.toml"),
"version = 1\npublic_url = \"https://tit.example/\"\n",
)
.expect("write the configuration");
let output = Command::new(env!("CARGO_BIN_EXE_tit"))
.arg("--user")
.env("XDG_DATA_HOME", data.path())
.output()
.expect("run tit");
assert!(output.status.success());
assert!(output.stdout.is_empty());
assert!(output.stderr.is_empty());
}
#[test]
fn test_support_allocates_a_port_and_external_client_fixtures() {
let instance = TestInstance::new();
assert_ne!(free_address().port(), 0);
for object_format in ["sha1", "sha256"] {
create_bare_git_fixture(
&instance.path().join(format!("{object_format}.git")),
object_format,
);
}
create_ssh_key_fixture(&instance.path().join("test-key"));
read_stock_ssh_configuration();
}
#[test]
fn setup_creates_one_administrator_and_shows_one_recovery_code() {
let instance = TestInstance::new();
let private_key = instance.path().join("administrator");
create_ssh_key_fixture(&private_key);
let public_key = fs::read_to_string(private_key.with_extension("pub"))
.expect("read the administrator public key");
let arguments = [
"--config",
instance.config().to_str().expect("a UTF-8 path"),
"setup",
"admin",
"alice",
public_key.trim(),
];
let first = instance.run(&arguments);
assert!(
first.status.success(),
"setup failed: {}",
String::from_utf8_lossy(&first.stderr)
);
assert!(first.stderr.is_empty());
let output = String::from_utf8(first.stdout).expect("read setup output");
let recovery_code = output
.strip_prefix("Recovery code: tit-recovery-v1:")
.and_then(|value| value.strip_suffix('\n'))
.expect("read the recovery code");
assert_eq!(recovery_code.len(), 64);
assert!(
recovery_code
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
);
let database_path = instance.path().join("tit.sqlite3");
assert_eq!(
fs::metadata(&database_path)
.expect("inspect the database")
.permissions()
.mode()
& 0o777,
0o600
);
let database = rusqlite::Connection::open(&database_path).expect("open the database");
let record: (String, i64, String, String, Vec<u8>) = database
.query_row(
"SELECT account.username, account.is_administrator,
ssh_public_key.canonical_key, ssh_public_key.fingerprint,
recovery_credential.credential_hash
FROM account
JOIN ssh_public_key ON ssh_public_key.account_id = account.id
JOIN recovery_credential ON recovery_credential.account_id = account.id",
[],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
},
)
.expect("read the administrator");
assert_eq!(record.0, "alice");
assert_eq!(record.1, 1);
assert_eq!(record.2.split_whitespace().count(), 2);
assert!(record.3.starts_with("SHA256:"));
let full_code = format!("tit-recovery-v1:{recovery_code}");
assert_eq!(record.4, Sha256::digest(full_code.as_bytes()).as_slice());
for path in [
database_path.clone(),
database_path.with_extension("sqlite3-wal"),
] {
if let Ok(bytes) = fs::read(path) {
assert!(
!bytes
.windows(full_code.len())
.any(|value| value == full_code.as_bytes()),
"the database contains the clear recovery code"
);
}
}
let second = instance.run(&arguments);
assert_eq!(second.status.code(), Some(1));
assert!(second.stdout.is_empty());
assert!(String::from_utf8_lossy(&second.stderr).contains("already has an administrator"));
assert_eq!(
database
.query_row("SELECT count(*) FROM account", [], |row| row
.get::<_, i64>(0))
.expect("count accounts"),
1
);
}
#[test]
fn setup_rejects_invalid_identity_before_it_creates_a_database() {
for (username, key) in [
("Alice", "not a key"),
("admin", "not a key"),
("bad_name", "not a key"),
("alice", "not a key"),
] {
let instance = TestInstance::new();
let output = instance.run(&[
"--config",
instance.config().to_str().expect("a UTF-8 path"),
"setup",
"admin",
username,
key,
]);
assert_eq!(output.status.code(), Some(1));
assert!(output.stdout.is_empty());
assert!(!instance.path().join("tit.sqlite3").exists());
}
}
#[test]
fn concurrent_setup_creates_exactly_one_administrator() {
let instance = TestInstance::new();
let first_private = instance.path().join("first-key");
let second_private = instance.path().join("second-key");
create_ssh_key_fixture(&first_private);
create_ssh_key_fixture(&second_private);
let first_key =
fs::read_to_string(first_private.with_extension("pub")).expect("read the first public key");
let second_key = fs::read_to_string(second_private.with_extension("pub"))
.expect("read the second public key");
let start = |username: &str, key: &str| {
Command::new(env!("CARGO_BIN_EXE_tit"))
.args([
"--config",
instance.config().to_str().expect("a UTF-8 path"),
"setup",
"admin",
username,
key.trim(),
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("start concurrent setup")
};
let mut first = start("alice", &first_key);
let mut second = start("bob", &second_key);
let first = first.wait().expect("wait for the first setup");
let second = second.wait().expect("wait for the second setup");
assert_ne!(first.success(), second.success());
let database = rusqlite::Connection::open(instance.path().join("tit.sqlite3"))
.expect("open the setup database");
assert_eq!(
database
.query_row("SELECT count(*) FROM account", [], |row| row
.get::<_, i64>(0))
.expect("count accounts"),
1
);
}
#[test]
fn maintenance_runs_offline_and_rejects_a_zero_retention_period() {
let instance = TestInstance::new();
create_administrator(&instance, "alice");
let config = instance.config().to_str().expect("a UTF-8 path");
let maintained = instance.run(&["--config", config, "maintenance"]);
assert!(
maintained.status.success(),
"maintenance failed: {}",
String::from_utf8_lossy(&maintained.stderr)
);
assert!(String::from_utf8_lossy(&maintained.stdout).starts_with("Pruned "));
let rejected = instance.run(&["--config", config, "maintenance", "--retention-days", "0"]);
assert_eq!(rejected.status.code(), Some(1));
assert!(
String::from_utf8_lossy(&rejected.stderr)
.contains("retention days must be greater than zero")
);
}
#[test]
fn administers_repositories_with_immutable_canonical_paths() {
let instance = TestInstance::new();
create_administrator(&instance, "alice");
let config = instance.config().to_str().expect("a UTF-8 path");
let created = Command::new(env!("CARGO_BIN_EXE_tit"))
.args([
"--config",
config,
"admin",
"repository",
"create",
"alice",
"project",
])
.env("PATH", "")
.output()
.expect("create a repository without runtime Git");
assert!(
created.status.success(),
"create failed: {}",
String::from_utf8_lossy(&created.stderr)
);
let created = repository_output(&created.stdout);
assert_eq!(created.get("owner").map(String::as_str), Some("alice"));
assert_eq!(created.get("slug").map(String::as_str), Some("project"));
assert_eq!(
created.get("visibility").map(String::as_str),
Some("public")
);
assert_eq!(created.get("state").map(String::as_str), Some("active"));
assert!(!created.contains_key("object-format"));
let id = created.get("id").expect("read the repository ID");
assert_eq!(id.len(), 32);
assert!(id.bytes().all(|byte| byte.is_ascii_hexdigit()));
let path = PathBuf::from(created.get("path").expect("read the repository path"));
let repository_root = fs::canonicalize(instance.path().join("repositories"))
.expect("canonicalize managed repositories");
assert_eq!(path.parent(), Some(repository_root.as_path()));
let expected_name = format!("{id}.git");
assert_eq!(
path.file_name().and_then(|name| name.to_str()),
Some(expected_name.as_str())
);
assert_eq!(
fs::read_to_string(path.join("HEAD")).expect("read repository HEAD"),
"ref: refs/heads/main\n"
);
let renamed = instance.run(&[
"--config",
config,
"admin",
"repository",
"rename",
"alice",
"project",
"renamed",
]);
assert!(renamed.status.success());
let renamed = repository_output(&renamed.stdout);
assert_eq!(renamed.get("slug").map(String::as_str), Some("renamed"));
assert_eq!(renamed.get("id"), Some(id));
assert_eq!(renamed.get("path"), created.get("path"));
let old_name = instance.run(&[
"--config",
config,
"admin",
"repository",
"inspect",
"alice",
"project",
]);
assert_eq!(old_name.status.code(), Some(1));
let archived = instance.run(&[
"--config",
config,
"admin",
"repository",
"archive",
"alice",
"renamed",
]);
assert!(archived.status.success());
let archived = repository_output(&archived.stdout);
assert_eq!(archived.get("state").map(String::as_str), Some("archived"));
assert_ne!(archived.get("archived-at").map(String::as_str), Some("-"));
let second_archive = instance.run(&[
"--config",
config,
"admin",
"repository",
"archive",
"alice",
"renamed",
]);
assert_eq!(second_archive.status.code(), Some(1));
let duplicate = instance.run(&[
"--config",
config,
"admin",
"repository",
"create",
"alice",
"renamed",
]);
assert_eq!(duplicate.status.code(), Some(1));
let missing_owner = instance.run(&[
"--config",
config,
"admin",
"repository",
"create",
"bob",
"project",
]);
assert_eq!(missing_owner.status.code(), Some(1));
}
#[test]
fn administers_repository_visibility_and_collaborators() {
let instance = TestInstance::new();
create_administrator(&instance, "alice");
let config = instance.config().to_str().expect("a UTF-8 path");
let created = instance.run(&[
"--config",
config,
"admin",
"repository",
"create",
"alice",
"project",
]);
assert!(created.status.success());
let database = rusqlite::Connection::open(instance.path().join("tit.sqlite3"))
.expect("open the repository database");
database
.execute(
"INSERT INTO account (username, is_administrator, state, created_at)
VALUES ('bob', 0, 'active', 1)",
[],
)
.expect("create a collaborator account");
drop(database);
let visibility = instance.run(&[
"--config",
config,
"admin",
"repository",
"visibility",
"alice",
"project",
"private",
]);
assert!(visibility.status.success());
assert_eq!(
repository_output(&visibility.stdout)
.get("visibility")
.map(String::as_str),
Some("private")
);
let collaborator = instance.run(&[
"--config",
config,
"admin",
"repository",
"collaborator-set",
"alice",
"project",
"bob",
"reader",
]);
assert!(collaborator.status.success());
let database = rusqlite::Connection::open(instance.path().join("tit.sqlite3"))
.expect("open the repository database");
let role: String = database
.query_row(
"SELECT role FROM repository_collaborator
JOIN account ON account.id = repository_collaborator.account_id
WHERE account.username = 'bob'",
[],
|row| row.get(0),
)
.expect("read the collaborator role");
assert_eq!(role, "reader");
drop(database);
let removed = instance.run(&[
"--config",
config,
"admin",
"repository",
"collaborator-remove",
"alice",
"project",
"bob",
]);
assert!(removed.status.success());
let database = rusqlite::Connection::open(instance.path().join("tit.sqlite3"))
.expect("open the repository database");
let collaborators: i64 = database
.query_row("SELECT count(*) FROM repository_collaborator", [], |row| {
row.get(0)
})
.expect("count repository collaborators");
assert_eq!(collaborators, 0);
drop(database);
let rejected_remove = instance.run(&[
"--config",
config,
"admin",
"repository",
"collaborator-remove",
"alice",
"project",
"bob",
]);
assert_eq!(rejected_remove.status.code(), Some(1));
let audit = instance.run(&["--config", config, "admin", "audit", "--limit", "10"]);
assert!(audit.status.success());
let audit = String::from_utf8(audit.stdout).expect("read the audit history");
assert!(audit.contains("action=repository.create\n"));
assert!(audit.contains("action=repository.visibility\n"));
assert!(audit.contains("action=collaborator.set\n"));
assert!(audit.contains("action=collaborator.remove\n"));
assert!(audit.contains("actor=admin-cli\n"));
assert!(audit.contains("outcome=success\n"));
assert!(audit.contains("outcome=failure\n"));
assert!(audit.contains("correlation-id="));
}
#[test]
fn imports_bare_repositories_and_rejects_unsafe_sources() {
let instance = TestInstance::new();
create_administrator(&instance, "alice");
let config = instance.config().to_str().expect("a UTF-8 path");
let source = instance.path().join("source.git");
create_bare_git_fixture(&source, "sha1");
fs::write(source.join("HEAD"), "ref: refs/heads/trunk\n")
.expect("set the imported symbolic HEAD");
let imported = instance.run(&[
"--config",
config,
"admin",
"repository",
"import",
"alice",
"imported",
source.to_str().expect("a UTF-8 path"),
]);
assert!(
imported.status.success(),
"import failed: {}",
String::from_utf8_lossy(&imported.stderr)
);
let imported = repository_output(&imported.stdout);
assert!(!imported.contains_key("object-format"));
assert_ne!(imported.get("path"), Some(&source.display().to_string()));
let database = rusqlite::Connection::open(instance.path().join("tit.sqlite3"))
.expect("open the imported repository database");
let default_branch: String = database
.query_row(
"SELECT repository_default_branch.ref_name
FROM repository_default_branch
JOIN repository ON repository.id = repository_default_branch.repository_id
WHERE repository.slug = 'imported'",
[],
|row| row.get(0),
)
.expect("read the imported default branch");
assert_eq!(default_branch, "refs/heads/trunk");
let unsafe_source = instance.path().join("unsafe.git");
create_bare_git_fixture(&unsafe_source, "sha1");
std::os::unix::fs::symlink("HEAD", unsafe_source.join("unsafe-link"))
.expect("create a repository symlink");
let unsafe_import = instance.run(&[
"--config",
config,
"admin",
"repository",
"import",
"alice",
"unsafe",
unsafe_source.to_str().expect("a UTF-8 path"),
]);
assert_eq!(unsafe_import.status.code(), Some(1));
assert!(unsafe_import.stdout.is_empty());
assert!(
fs::read_dir(instance.path().join("repositories"))
.expect("read managed repositories")
.all(|entry| !entry
.expect("read a managed repository")
.file_name()
.to_string_lossy()
.starts_with(".pending-"))
);
for slug in ["Project", "project.git", "../project", "admin"] {
let rejected = instance.run(&[
"--config",
config,
"admin",
"repository",
"create",
"alice",
slug,
]);
assert_eq!(rejected.status.code(), Some(1), "accepted {slug:?}");
assert!(rejected.stdout.is_empty());
}
}
#[test]
fn removes_an_import_after_each_post_rename_failure() {
for point in [
"after-rename",
"after-canonicalize",
"after-object-format",
"after-open",
"after-references",
] {
let instance = TestInstance::new();
create_administrator(&instance, "alice");
let config = instance.config().to_str().expect("a UTF-8 path");
let source = instance.path().join("source.git");
create_bare_git_fixture(&source, "sha1");
let output = Command::new(env!("CARGO_BIN_EXE_tit"))
.args([
"--config",
config,
"admin",
"repository",
"import",
"alice",
"failed",
source.to_str().expect("a UTF-8 path"),
])
.env("TIT_TEST_IMPORT_FAIL", point)
.output()
.expect("run the faulted import");
assert_eq!(output.status.code(), Some(1), "{point}");
assert!(
fs::read_dir(instance.path().join("repositories"))
.expect("read managed repositories")
.all(|entry| entry
.expect("read a managed repository")
.file_name()
.to_string_lossy()
.starts_with('.')),
"{point} left an unmanaged repository"
);
let inspected = instance.run(&[
"--config",
config,
"admin",
"repository",
"inspect",
"alice",
"failed",
]);
assert_eq!(inspected.status.code(), Some(1), "{point}");
}
}
#[test]
fn doctor_inspect_and_dump_are_read_only_and_detect_operational_damage() {
let instance = TestInstance::new();
create_administrator(&instance, "alice");
let config = instance.config().to_str().expect("a UTF-8 path");
let created = instance.run(&[
"--config",
config,
"admin",
"repository",
"create",
"alice",
"project",
]);
assert!(created.status.success());
create_ssh_key_fixture(&instance.path().join("ssh_host_ed25519_key"));
let doctor = instance.run(&["--config", config, "doctor"]);
assert!(
doctor.status.success(),
"doctor failed: {}",
String::from_utf8_lossy(&doctor.stderr)
);
assert!(doctor.stdout.is_empty());
let account = instance.run(&["--config", config, "inspect", "account", "alice"]);
assert!(account.status.success());
let account: serde_json::Value =
serde_json::from_slice(&account.stdout).expect("parse the account inspection");
assert_eq!(account["type"], "account");
assert_eq!(account["username"], "alice");
assert_eq!(account["keys"].as_array().map(Vec::len), Some(1));
let repository = instance.run(&[
"--config",
config,
"inspect",
"repository",
"alice",
"project",
]);
assert!(repository.status.success());
let repository: serde_json::Value =
serde_json::from_slice(&repository.stdout).expect("parse the repository inspection");
assert_eq!(repository["type"], "repository");
assert_eq!(repository["object_format"], "sha1");
let repository_id = repository["id"]
.as_str()
.expect("read the repository identifier")
.to_owned();
let first_dump = instance.run(&["--config", config, "dump"]);
let second_dump = instance.run(&["--config", config, "dump"]);
assert!(first_dump.status.success());
assert_eq!(first_dump.stdout, second_dump.stdout);
let dump_rows = String::from_utf8(first_dump.stdout).expect("read the JSON Lines dump");
assert!(!dump_rows.is_empty());
for line in dump_rows.lines() {
let row: serde_json::Value = serde_json::from_str(line).expect("parse a dump row");
assert_eq!(row["type"], "sqlite-row");
assert!(row["table"].is_string());
assert!(row["columns"].is_array());
}
assert!(dump_rows.contains("\"type\":\"blob-hex\""));
let backup_directory = TempDir::new().expect("create a backup directory");
let backup = backup_directory.path().join("instance.tar");
let backup_output = instance.run(&[
"--config",
config,
"backup",
backup.to_str().expect("a UTF-8 backup path"),
]);
assert!(backup_output.status.success());
let checked_backup = instance.run(&[
"--config",
config,
"doctor",
"--backup",
backup.to_str().expect("a UTF-8 backup path"),
]);
assert!(checked_backup.status.success());
let database_path = instance.path().join("tit.sqlite3");
let database = rusqlite::Connection::open(&database_path).expect("open the database");
let repository_path = instance
.path()
.join("repositories")
.join(format!("{repository_id}.git"));
let intent_id = "11111111111111111111111111111111";
let quarantine = repository_path
.join("objects")
.join("tit-quarantine")
.join(intent_id);
database
.execute(
"INSERT INTO git_operation_intent
(id, repository_path, actor, initial_refs, proposed_refs, event_payload,
quarantine_path, state, pack_name, created_at)
VALUES (?1, ?2, 'alice', X'', X'', X'', ?3, 'pending', NULL, 1)",
rusqlite::params![
intent_id,
repository_path.to_str().expect("a UTF-8 repository path"),
quarantine.to_str().expect("a UTF-8 quarantine path")
],
)
.expect("create an incomplete intent");
drop(database);
let incomplete = instance.run(&["--config", config, "doctor"]);
assert_eq!(incomplete.status.code(), Some(1));
assert!(String::from_utf8_lossy(&incomplete.stderr).contains("incomplete intents"));
let intent = instance.run(&["--config", config, "inspect", "intent", intent_id]);
assert!(intent.status.success());
let intent: serde_json::Value =
serde_json::from_slice(&intent.stdout).expect("parse the intent inspection");
assert_eq!(intent["state"], "pending");
let database = rusqlite::Connection::open(&database_path).expect("reopen the database");
assert_eq!(
database
.query_row(
"SELECT state FROM git_operation_intent WHERE id = ?1",
[intent_id],
|row| row.get::<_, String>(0),
)
.expect("read the intent state"),
"pending"
);
drop(database);
let repaired_intent = instance.run(&["--config", config, "repair", "intents"]);
assert!(
repaired_intent.status.success(),
"intent repair failed: {}",
String::from_utf8_lossy(&repaired_intent.stderr)
);
let database = rusqlite::Connection::open(&database_path).expect("reopen the database");
assert_eq!(
database
.query_row(
"SELECT state FROM git_operation_intent WHERE id = ?1",
[intent_id],
|row| row.get::<_, String>(0),
)
.expect("read the repaired intent state"),
"abandoned"
);
drop(database);
fs::create_dir_all(&quarantine).expect("create quarantine debris");
fs::write(quarantine.join("incoming.pack"), b"debris").expect("write quarantine debris");
let debris = instance.run(&["--config", config, "doctor"]);
assert_eq!(debris.status.code(), Some(1));
assert!(String::from_utf8_lossy(&debris.stderr).contains("quarantine debris"));
let repaired_quarantine = instance.run(&["--config", config, "repair", "quarantine"]);
assert!(
repaired_quarantine.status.success(),
"quarantine repair failed: {}",
String::from_utf8_lossy(&repaired_quarantine.stderr)
);
let database = rusqlite::Connection::open(&database_path).expect("reopen the database");
database
.execute_batch("DROP INDEX audit_event_history")
.expect("remove a required index");
drop(database);
let missing_index = instance.run(&["--config", config, "doctor"]);
assert_eq!(missing_index.status.code(), Some(1));
assert!(String::from_utf8_lossy(&missing_index.stderr).contains("audit_event_history"));
let database = rusqlite::Connection::open(&database_path).expect("reopen the database");
database
.execute_batch(
"CREATE INDEX audit_event_history
ON audit_event (created_at DESC, id DESC)",
)
.expect("restore the required index");
drop(database);
let bad_ref = repository_path.join("refs").join("heads").join("broken");
fs::create_dir_all(bad_ref.parent().expect("the ref has a parent"))
.expect("create the ref directory");
fs::write(&bad_ref, b"deadbeef\n").expect("damage a Git ref");
let damaged_git = instance.run(&["--config", config, "doctor"]);
assert_eq!(damaged_git.status.code(), Some(1));
fs::remove_file(&bad_ref).expect("remove the damaged ref");
fs::set_permissions(instance.config(), fs::Permissions::from_mode(0o644))
.expect("make the configuration unsafe");
let unsafe_permissions = instance.run(&["--config", config, "doctor"]);
assert_eq!(unsafe_permissions.status.code(), Some(1));
assert!(String::from_utf8_lossy(&unsafe_permissions.stderr).contains("owner-only"));
fs::set_permissions(instance.config(), fs::Permissions::from_mode(0o600))
.expect("restore the configuration permissions");
let mut backup_bytes = fs::read(&backup).expect("read the backup");
let offset = backup_bytes
.windows(b"version = 1".len())
.position(|candidate| candidate == b"version = 1")
.expect("find archived configuration data");
backup_bytes[offset] ^= 1;
fs::write(&backup, backup_bytes).expect("damage the backup");
let damaged_backup = instance.run(&[
"--config",
config,
"doctor",
"--backup",
backup.to_str().expect("a UTF-8 backup path"),
]);
assert_eq!(damaged_backup.status.code(), Some(1));
}
fn create_administrator(instance: &TestInstance, username: &str) {
let private_key = instance.path().join("admin-key");
create_ssh_key_fixture(&private_key);
let public_key = fs::read_to_string(private_key.with_extension("pub"))
.expect("read the administrator public key");
let output = instance.run(&[
"--config",
instance.config().to_str().expect("a UTF-8 path"),
"setup",
"admin",
username,
public_key.trim(),
]);
assert!(output.status.success());
}
fn repository_output(output: &[u8]) -> std::collections::BTreeMap<String, String> {
String::from_utf8(output.to_vec())
.expect("read repository output")
.lines()
.map(|line| {
let (name, value) = line.split_once('=').expect("read a repository field");
(name.to_owned(), value.to_owned())
})
.collect()
}