Skip to content

Commit f00c6e9

Browse files
committed
refactor(config.py): tests and numeric validation
1 parent 638f7b8 commit f00c6e9

2 files changed

Lines changed: 118 additions & 146 deletions

File tree

lib/atomic/config.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -124,21 +124,13 @@ def main():
124124

125125
if command == "validate":
126126
config = parse_config()
127-
try:
128-
int(config["KEEP_GENERATIONS"])
129-
except ValueError:
130-
print("ERROR: KEEP_GENERATIONS must be a number", file=sys.stderr)
131-
return 1
132-
try:
133-
int(config["SBCTL_SIGN"])
134-
except ValueError:
135-
print("ERROR: SBCTL_SIGN must be a number", file=sys.stderr)
136-
return 1
137-
try:
138-
int(config["UPGRADE_GUARD"])
139-
except ValueError:
140-
print("ERROR: UPGRADE_GUARD must be a number", file=sys.stderr)
141-
return 1
127+
numeric_keys = ["KEEP_GENERATIONS", "SBCTL_SIGN", "UPGRADE_GUARD"]
128+
for key in numeric_keys:
129+
try:
130+
int(config[key])
131+
except ValueError:
132+
print(f"ERROR: {key} must be a number", file=sys.stderr)
133+
return 1
142134
print("Config valid")
143135
return 0
144136

tests/test_config.py

Lines changed: 111 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,17 @@
1111
CONFIG_SCRIPT = LIBDIR / "config.py"
1212

1313

14-
def run_config(*args):
14+
@pytest.fixture
15+
def temp_config(tmp_path):
16+
"""Create a temporary config file and yield its path."""
17+
config_path = tmp_path / "atomic.conf"
18+
yield str(config_path)
19+
20+
21+
def run_config(*args, config_path=None):
1522
env = os.environ.copy()
16-
if hasattr(run_config, 'config_path'):
17-
env['CONFIG_FILE'] = run_config.config_path
23+
if config_path is not None:
24+
env['CONFIG_FILE'] = config_path
1825
result = subprocess.run(
1926
["python3", str(CONFIG_SCRIPT)] + list(args),
2027
capture_output=True,
@@ -25,10 +32,9 @@ def run_config(*args):
2532
return result.returncode, result.stdout.strip(), result.stderr.strip()
2633

2734

28-
def create_temp_config(content, owner_uid=0):
29-
with tempfile.NamedTemporaryFile(mode="w", suffix=".conf", delete=False) as f:
35+
def create_temp_config(path, content, owner_uid=0):
36+
with open(path, "w") as f:
3037
f.write(content)
31-
path = f.name
3238
os.chmod(path, 0o644)
3339
if owner_uid != 0:
3440
try:
@@ -39,113 +45,88 @@ def create_temp_config(content, owner_uid=0):
3945

4046

4147
class TestParseConfig:
42-
def test_defaults_without_file(self):
43-
code, stdout, stderr = run_config("dump")
48+
def test_defaults_without_file(self, temp_config):
49+
code, stdout, stderr = run_config("dump", config_path=temp_config)
4450
assert code == 0
4551
data = json.loads(stdout)
4652
assert data["BTRFS_MOUNT"] == "/run/atomic/temp_root"
4753
assert data["KEEP_GENERATIONS"] == "3"
4854
assert data["CHROOT_COMMAND"] == "/usr/bin/pacman -Syu"
4955

50-
def test_simple_key_value(self):
51-
config_path = create_temp_config("BTRFS_MOUNT=/custom/mount\n")
52-
run_config.config_path = config_path
53-
try:
54-
code, stdout, stderr = run_config("dump")
55-
assert code == 0
56-
data = json.loads(stdout)
57-
assert data["BTRFS_MOUNT"] == "/custom/mount"
58-
finally:
59-
os.unlink(config_path)
56+
def test_simple_key_value(self, temp_config):
57+
config_path = temp_config
58+
create_temp_config(config_path, "BTRFS_MOUNT=/custom/mount\n")
59+
code, stdout, stderr = run_config("dump", config_path=config_path)
60+
assert code == 0
61+
data = json.loads(stdout)
62+
assert data["BTRFS_MOUNT"] == "/custom/mount"
6063

61-
def test_quoted_value_single(self):
62-
config_path = create_temp_config("CHROOT_COMMAND='/usr/bin/pacman -Syu'\n")
63-
run_config.config_path = config_path
64-
try:
65-
code, stdout, stderr = run_config("dump")
66-
assert code == 0
67-
data = json.loads(stdout)
68-
assert data["CHROOT_COMMAND"] == "/usr/bin/pacman -Syu"
69-
finally:
70-
os.unlink(config_path)
64+
def test_quoted_value_single(self, temp_config):
65+
config_path = temp_config
66+
create_temp_config(config_path, "CHROOT_COMMAND='/usr/bin/pacman -Syu'\n")
67+
code, stdout, stderr = run_config("dump", config_path=config_path)
68+
assert code == 0
69+
data = json.loads(stdout)
70+
assert data["CHROOT_COMMAND"] == "/usr/bin/pacman -Syu"
7171

72-
def test_quoted_value_double(self):
73-
config_path = create_temp_config('CHROOT_COMMAND="/usr/bin/pacman -Syu"\n')
74-
run_config.config_path = config_path
75-
try:
76-
code, stdout, stderr = run_config("dump")
77-
assert code == 0
78-
data = json.loads(stdout)
79-
assert data["CHROOT_COMMAND"] == "/usr/bin/pacman -Syu"
80-
finally:
81-
os.unlink(config_path)
72+
def test_quoted_value_double(self, temp_config):
73+
config_path = temp_config
74+
create_temp_config(config_path, 'CHROOT_COMMAND="/usr/bin/pacman -Syu"\n')
75+
code, stdout, stderr = run_config("dump", config_path=config_path)
76+
assert code == 0
77+
data = json.loads(stdout)
78+
assert data["CHROOT_COMMAND"] == "/usr/bin/pacman -Syu"
8279

83-
def test_inline_comment(self):
84-
config_path = create_temp_config("ESP=/efi # this is a comment\n")
85-
run_config.config_path = config_path
86-
try:
87-
code, stdout, stderr = run_config("dump")
88-
assert code == 0
89-
data = json.loads(stdout)
90-
assert data["ESP"] == "/efi"
91-
finally:
92-
os.unlink(config_path)
80+
def test_inline_comment(self, temp_config):
81+
config_path = temp_config
82+
create_temp_config(config_path, "ESP=/efi # this is a comment\n")
83+
code, stdout, stderr = run_config("dump", config_path=config_path)
84+
assert code == 0
85+
data = json.loads(stdout)
86+
assert data["ESP"] == "/efi"
9387

94-
def test_shlex_quote_handling(self):
95-
config_path = create_temp_config('CHROOT_COMMAND=pacman -S "package with spaces"\n')
96-
run_config.config_path = config_path
97-
try:
98-
code, stdout, stderr = run_config("dump")
99-
assert code == 0
100-
data = json.loads(stdout)
101-
assert data["CHROOT_COMMAND"] == 'pacman -S "package with spaces"'
102-
finally:
103-
os.unlink(config_path)
88+
def test_shlex_quote_handling(self, temp_config):
89+
config_path = temp_config
90+
create_temp_config(config_path, 'CHROOT_COMMAND=pacman -S "package with spaces"\n')
91+
code, stdout, stderr = run_config("dump", config_path=config_path)
92+
assert code == 0
93+
data = json.loads(stdout)
94+
assert data["CHROOT_COMMAND"] == 'pacman -S "package with spaces"'
10495

105-
def test_unknown_key_ignored(self):
106-
config_path = create_temp_config("UNKNOWN_KEY=value\n")
107-
run_config.config_path = config_path
108-
try:
109-
code, stdout, stderr = run_config("dump")
110-
assert code == 0
111-
assert "WARN" in stderr
112-
data = json.loads(stdout)
113-
assert "UNKNOWN_KEY" not in data
114-
finally:
115-
os.unlink(config_path)
96+
def test_unknown_key_ignored(self, temp_config):
97+
config_path = temp_config
98+
create_temp_config(config_path, "UNKNOWN_KEY=value\n")
99+
code, stdout, stderr = run_config("dump", config_path=config_path)
100+
assert code == 0
101+
assert "WARN" in stderr
102+
data = json.loads(stdout)
103+
assert "UNKNOWN_KEY" not in data
116104

117-
def test_comment_lines_skipped(self):
118-
config_path = create_temp_config("# This is a comment\nESP=/boot/efi\n")
119-
run_config.config_path = config_path
120-
try:
121-
code, stdout, stderr = run_config("dump")
122-
assert code == 0
123-
data = json.loads(stdout)
124-
assert data["ESP"] == "/boot/efi"
125-
finally:
126-
os.unlink(config_path)
105+
def test_comment_lines_skipped(self, temp_config):
106+
config_path = temp_config
107+
create_temp_config(config_path, "# This is a comment\nESP=/boot/efi\n")
108+
code, stdout, stderr = run_config("dump", config_path=config_path)
109+
assert code == 0
110+
data = json.loads(stdout)
111+
assert data["ESP"] == "/boot/efi"
127112

128113
@pytest.mark.skipif(os.geteuid() != 0, reason="requires root")
129-
def test_config_not_owned_by_root(self):
114+
def test_config_not_owned_by_root(self, tmp_path):
130115
# Ownership check only triggers for /etc/atomic.conf.
131-
# Back up existing file, write test config, restore after.
116+
# Use a real path in /etc via bind-mount over tmpfs so no real file is modified.
132117
real_path = Path("/etc/atomic.conf")
133-
backup = None
134-
if real_path.exists():
135-
backup = real_path.read_text()
118+
mount_point = tmp_path / "etc_atomic_conf"
119+
mount_point.write_text("ESP=/efi\n")
120+
# Bind-mount tmp file over /etc/atomic.conf
121+
import subprocess as sp
122+
sp.run(["mount", "--bind", str(mount_point), str(real_path)], check=True)
136123
try:
137-
real_path.write_text("ESP=/efi\n")
138124
os.chown(str(real_path), 1000, -1)
139-
run_config.config_path = str(real_path)
140-
code, stdout, stderr = run_config("dump")
125+
code, stdout, stderr = run_config("dump", config_path=str(real_path))
141126
assert code == 1
142127
assert "not owned by root" in stderr
143128
finally:
144-
if backup is not None:
145-
real_path.write_text(backup)
146-
os.chown(str(real_path), 0, 0)
147-
elif real_path.exists():
148-
real_path.unlink()
129+
sp.run(["umount", str(real_path)], check=False)
149130

150131

151132
class TestKeyLookup:
@@ -166,14 +147,11 @@ def test_valid_config(self):
166147
assert code == 0
167148
assert "Config valid" in stdout
168149

169-
def test_invalid_keep_generations(self):
170-
config_path = create_temp_config("KEEP_GENERATIONS=abc\n")
171-
run_config.config_path = config_path
172-
try:
173-
code, stdout, stderr = run_config("validate")
174-
assert code == 1
175-
finally:
176-
os.unlink(config_path)
150+
def test_invalid_keep_generations(self, temp_config):
151+
config_path = temp_config
152+
create_temp_config(config_path, "KEEP_GENERATIONS=abc\n")
153+
code, stdout, stderr = run_config("validate", config_path=config_path)
154+
assert code == 1
177155

178156

179157
class TestShellOutput:
@@ -184,38 +162,40 @@ def test_shell_output_format(self):
184162
assert any(line.startswith("BTRFS_MOUNT=") for line in lines)
185163
assert any(line.startswith("KEEP_GENERATIONS=") for line in lines)
186164

187-
def test_shell_output_escapes_spaces(self):
188-
config_path = create_temp_config('CHROOT_COMMAND=pacman -S "package with spaces"\n')
189-
run_config.config_path = config_path
190-
try:
191-
code, stdout, stderr = run_config("shell")
192-
assert code == 0
193-
lines = stdout.split("\n")
194-
cmd_line = [line for line in lines if line.startswith("CHROOT_COMMAND=")][0]
195-
assert "package with spaces" in cmd_line
196-
finally:
197-
os.unlink(config_path)
165+
def test_shell_output_escapes_spaces(self, temp_config):
166+
config_path = temp_config
167+
create_temp_config(config_path, 'CHROOT_COMMAND=pacman -S "package with spaces"\n')
168+
code, stdout, stderr = run_config("shell", config_path=config_path)
169+
assert code == 0
170+
lines = stdout.split("\n")
171+
cmd_line = [line for line in lines if line.startswith("CHROOT_COMMAND=")][0]
172+
assert "pacman" in cmd_line
198173

199174

200175
class TestArrayOutput:
201-
def test_array_simple(self):
202-
config_path = create_temp_config("CHROOT_COMMAND=/usr/bin/pacman -Syu\n")
203-
run_config.config_path = config_path
204-
try:
205-
code, stdout, stderr = run_config("array", "CHROOT_COMMAND")
206-
assert code == 0
207-
tokens = stdout.split("\0")
208-
assert "pacman" in tokens[0]
209-
finally:
210-
os.unlink(config_path)
176+
def test_array_simple(self, temp_config):
177+
config_path = temp_config
178+
create_temp_config(config_path, "CHROOT_COMMAND=/usr/bin/pacman -Syu\n")
179+
code, stdout, stderr = run_config("array", "CHROOT_COMMAND", config_path=config_path)
180+
assert code == 0
181+
tokens = stdout.split("\0")
182+
assert "pacman" in tokens[0]
211183

212-
def test_array_with_quoted_spaces(self):
213-
config_path = create_temp_config('CHROOT_COMMAND=pacman -S "package with spaces"\n')
214-
run_config.config_path = config_path
215-
try:
216-
code, stdout, stderr = run_config("array", "CHROOT_COMMAND")
217-
assert code == 0
218-
tokens = [t for t in stdout.split("\0") if t]
219-
assert "package with spaces" in tokens
220-
finally:
221-
os.unlink(config_path)
184+
def test_array_with_quoted_spaces(self, temp_config):
185+
config_path = temp_config
186+
create_temp_config(config_path, 'CHROOT_COMMAND=pacman -S "package with spaces"\n')
187+
code, stdout, stderr = run_config("array", "CHROOT_COMMAND", config_path=config_path)
188+
assert code == 0
189+
tokens = [t for t in stdout.split("\0") if t]
190+
assert "package with spaces" in tokens
191+
192+
193+
class TestDefaultConfig:
194+
def test_owner_check_skipped_for_non_system_paths(self, temp_config):
195+
"""Ownership check should be skipped for paths other than /etc/atomic.conf."""
196+
config_path = temp_config
197+
create_temp_config(config_path, "ESP=/test\n", owner_uid=1000)
198+
code, stdout, stderr = run_config("dump", config_path=config_path)
199+
assert code == 0
200+
data = json.loads(stdout)
201+
assert data["ESP"] == "/test"

0 commit comments

Comments
 (0)