mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
96f9c40ca3
Replace 13 local copies across all test files with a shared import from tests/common/mod.rs. Fix getuid/geteuid inconsistency in test_useradd.rs and test_userdel.rs (effective UID is correct for setuid-root tools). Fixes #133
279 lines
8.1 KiB
Rust
279 lines
8.1 KiB
Rust
// This file is part of the shadow-rs package.
|
|
//
|
|
// For the full copyright and license information, please view the LICENSE
|
|
// file that was distributed with this source code.
|
|
// spell-checker:ignore groupmod gshadow testgrp newgrp oldgrp nogroup
|
|
|
|
//! Integration tests for the `groupmod` utility.
|
|
//!
|
|
//! Tests that require root are guarded by `common::skip_unless_root()` and run inside
|
|
//! Docker CI containers. Non-root tests exercise clap parsing and error paths
|
|
//! that do not need privilege.
|
|
|
|
use std::ffi::OsString;
|
|
|
|
#[path = "../common/mod.rs"]
|
|
mod common;
|
|
|
|
/// Run `uumain` with the given args, returning the exit code.
|
|
fn run(args: &[&str]) -> i32 {
|
|
let os_args: Vec<OsString> = args.iter().map(|s| (*s).into()).collect();
|
|
groupmod::uumain(os_args.into_iter())
|
|
}
|
|
|
|
/// Helper to create a temp dir with `etc/group`, `etc/gshadow`, and `etc/passwd`.
|
|
fn setup_root(
|
|
group_content: &str,
|
|
gshadow_content: &str,
|
|
passwd_content: &str,
|
|
) -> tempfile::TempDir {
|
|
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
let etc = dir.path().join("etc");
|
|
std::fs::create_dir_all(&etc).expect("failed to create etc dir");
|
|
std::fs::write(etc.join("group"), group_content).expect("failed to write group file");
|
|
std::fs::write(etc.join("gshadow"), gshadow_content).expect("failed to write gshadow file");
|
|
std::fs::write(etc.join("passwd"), passwd_content).expect("failed to write passwd file");
|
|
dir
|
|
}
|
|
|
|
/// Read the group file content back from a prefix dir.
|
|
fn read_group(dir: &tempfile::TempDir) -> String {
|
|
std::fs::read_to_string(dir.path().join("etc/group")).expect("failed to read group file")
|
|
}
|
|
|
|
/// Read the gshadow file content back from a prefix dir.
|
|
fn read_gshadow(dir: &tempfile::TempDir) -> String {
|
|
std::fs::read_to_string(dir.path().join("etc/gshadow")).expect("failed to read gshadow file")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Non-root tests — exercise clap parsing and error paths
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn test_help_exits_zero() {
|
|
let code = run(&["groupmod", "--help"]);
|
|
assert_eq!(code, 0, "--help should exit 0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_unknown_flag_exits_error() {
|
|
let code = run(&["groupmod", "--nonexistent-flag"]);
|
|
assert_eq!(code, 2, "unknown flag should exit 2");
|
|
}
|
|
|
|
#[test]
|
|
fn test_missing_group_name_exits_error() {
|
|
let code = run(&["groupmod"]);
|
|
assert_eq!(code, 2, "missing group name should exit 2");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Root-only tests — exercise real operations via -P (prefix) flag
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn test_change_gid() {
|
|
if common::skip_unless_root() {
|
|
return;
|
|
}
|
|
|
|
let dir = setup_root(
|
|
"testgrp:x:1000:\n",
|
|
"testgrp:!::\n",
|
|
"root:x:0:0:root:/root:/bin/bash\n",
|
|
);
|
|
|
|
let prefix = dir.path().to_str().expect("temp dir path is valid UTF-8");
|
|
let code = run(&["groupmod", "-g", "9999", "-P", prefix, "testgrp"]);
|
|
assert_eq!(code, 0, "changing GID should exit 0");
|
|
|
|
let content = read_group(&dir);
|
|
assert!(
|
|
content.contains("testgrp:x:9999:"),
|
|
"GID should be changed to 9999, got: {content}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_change_name() {
|
|
if common::skip_unless_root() {
|
|
return;
|
|
}
|
|
|
|
let dir = setup_root(
|
|
"oldgrp:x:1000:\n",
|
|
"oldgrp:!::\n",
|
|
"root:x:0:0:root:/root:/bin/bash\n",
|
|
);
|
|
|
|
let prefix = dir.path().to_str().expect("temp dir path is valid UTF-8");
|
|
let code = run(&["groupmod", "-n", "newname", "-P", prefix, "oldgrp"]);
|
|
assert_eq!(code, 0, "renaming group should exit 0");
|
|
|
|
let content = read_group(&dir);
|
|
assert!(
|
|
content.contains("newname:x:1000:"),
|
|
"group should be renamed to newname, got: {content}"
|
|
);
|
|
assert!(
|
|
!content.contains("oldgrp"),
|
|
"old group name should not remain"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_non_unique_gid() {
|
|
if common::skip_unless_root() {
|
|
return;
|
|
}
|
|
|
|
let dir = setup_root(
|
|
"root:x:0:\ntestgrp:x:1000:\n",
|
|
"root:!::\ntestgrp:!::\n",
|
|
"root:x:0:0:root:/root:/bin/bash\n",
|
|
);
|
|
|
|
let prefix = dir.path().to_str().expect("temp dir path is valid UTF-8");
|
|
|
|
// Without -o, setting GID to 0 (already used by root) should fail.
|
|
let code = run(&["groupmod", "-g", "0", "-P", prefix, "testgrp"]);
|
|
assert_eq!(
|
|
code, 4,
|
|
"duplicate GID without -o should exit 4 (GID_IN_USE)"
|
|
);
|
|
|
|
// With -o, it should succeed.
|
|
let code = run(&["groupmod", "-o", "-g", "0", "-P", prefix, "testgrp"]);
|
|
assert_eq!(code, 0, "duplicate GID with -o should exit 0");
|
|
|
|
let content = read_group(&dir);
|
|
assert!(
|
|
content.contains("testgrp:x:0:"),
|
|
"GID should be changed to 0 with -o, got: {content}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_nonexistent_group_fails() {
|
|
if common::skip_unless_root() {
|
|
return;
|
|
}
|
|
|
|
let dir = setup_root(
|
|
"root:x:0:\n",
|
|
"root:!::\n",
|
|
"root:x:0:0:root:/root:/bin/bash\n",
|
|
);
|
|
|
|
let prefix = dir.path().to_str().expect("temp dir path is valid UTF-8");
|
|
let code = run(&["groupmod", "-g", "5000", "-P", prefix, "nogroup"]);
|
|
assert_eq!(
|
|
code, 6,
|
|
"modifying a nonexistent group should exit 6 (GROUP_NOT_FOUND)"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rename_preserves_members() {
|
|
if common::skip_unless_root() {
|
|
return;
|
|
}
|
|
|
|
let dir = setup_root(
|
|
"devteam:x:1500:alice,bob\n",
|
|
"devteam:!::alice,bob\n",
|
|
"alice:x:1000:1000:Alice:/home/alice:/bin/bash\nbob:x:1001:1001:Bob:/home/bob:/bin/bash\n",
|
|
);
|
|
|
|
let prefix = dir.path().to_str().expect("temp dir path is valid UTF-8");
|
|
let code = run(&["groupmod", "-n", "engineering", "-P", prefix, "devteam"]);
|
|
assert_eq!(code, 0, "renaming group with members should exit 0");
|
|
|
|
let content = read_group(&dir);
|
|
assert!(
|
|
content.contains("engineering:x:1500:alice,bob"),
|
|
"members should be preserved after rename, got: {content}"
|
|
);
|
|
assert!(
|
|
!content.contains("devteam"),
|
|
"old group name should not remain"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rename_updates_gshadow() {
|
|
if common::skip_unless_root() {
|
|
return;
|
|
}
|
|
|
|
let dir = setup_root(
|
|
"mygrp:x:2000:user1\n",
|
|
"mygrp:!::user1\n",
|
|
"root:x:0:0:root:/root:/bin/bash\n",
|
|
);
|
|
|
|
let prefix = dir.path().to_str().expect("temp dir path is valid UTF-8");
|
|
let code = run(&["groupmod", "-n", "renamed", "-P", prefix, "mygrp"]);
|
|
assert_eq!(code, 0, "renaming should update gshadow");
|
|
|
|
let gs_content = read_gshadow(&dir);
|
|
assert!(
|
|
gs_content.contains("renamed:"),
|
|
"gshadow should reflect the new name, got: {gs_content}"
|
|
);
|
|
assert!(
|
|
!gs_content.contains("mygrp"),
|
|
"old name should not remain in gshadow"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_name_collision_fails() {
|
|
if common::skip_unless_root() {
|
|
return;
|
|
}
|
|
|
|
let dir = setup_root(
|
|
"grp1:x:1000:\ngrp2:x:2000:\n",
|
|
"grp1:!::\ngrp2:!::\n",
|
|
"root:x:0:0:root:/root:/bin/bash\n",
|
|
);
|
|
|
|
let prefix = dir.path().to_str().expect("temp dir path is valid UTF-8");
|
|
let code = run(&["groupmod", "-n", "grp2", "-P", prefix, "grp1"]);
|
|
assert_eq!(
|
|
code, 9,
|
|
"renaming to an existing group name should exit 9 (NAME_IN_USE)"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_change_gid_and_name_together() {
|
|
if common::skip_unless_root() {
|
|
return;
|
|
}
|
|
|
|
let dir = setup_root(
|
|
"oldgrp:x:1000:member1\n",
|
|
"oldgrp:!::member1\n",
|
|
"root:x:0:0:root:/root:/bin/bash\n",
|
|
);
|
|
|
|
let prefix = dir.path().to_str().expect("temp dir path is valid UTF-8");
|
|
let code = run(&[
|
|
"groupmod", "-g", "7777", "-n", "freshgrp", "-P", prefix, "oldgrp",
|
|
]);
|
|
assert_eq!(code, 0, "changing both GID and name should exit 0");
|
|
|
|
let content = read_group(&dir);
|
|
assert!(
|
|
content.contains("freshgrp:x:7777:member1"),
|
|
"both GID and name should be updated, got: {content}"
|
|
);
|
|
assert!(
|
|
!content.contains("oldgrp"),
|
|
"old group name should not remain"
|
|
);
|
|
}
|