blob: 26d9e0ab540336e70a49cedbbac924cae01a49e1 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
|
#!/usr/bin/env bats
# Unit tests for installer/lib/config.sh
setup() {
# shellcheck disable=SC1091
source "${BATS_TEST_DIRNAME}/../../installer/lib/common.sh"
# shellcheck disable=SC1091
source "${BATS_TEST_DIRNAME}/../../installer/lib/raid.sh"
# shellcheck disable=SC1091
source "${BATS_TEST_DIRNAME}/../../installer/lib/config.sh"
}
@test "parse_args stores --config-file path" {
parse_args --config-file /tmp/foo.conf
[ "$CONFIG_FILE" = "/tmp/foo.conf" ]
}
@test "parse_args rejects --config-file with no argument" {
run parse_args --config-file
[ "$status" -eq 1 ]
[[ "$output" == *"requires a path"* ]]
}
@test "parse_args rejects --config-file when value looks like a flag" {
run parse_args --config-file --help
[ "$status" -eq 1 ]
[[ "$output" == *"requires a path"* ]]
}
@test "parse_args --color enables color vars" {
[ -z "$RED" ]
parse_args --color
[ -n "$RED" ]
}
@test "parse_args --help shows usage and exits 0" {
run parse_args --help
[ "$status" -eq 0 ]
[[ "$output" == *"Usage:"* ]]
[[ "$output" == *"--config-file"* ]]
}
@test "parse_args rejects unknown option" {
run parse_args --not-a-real-flag
[ "$status" -eq 1 ]
[[ "$output" == *"Unknown option"* ]]
}
@test "load_config errors on missing file" {
run load_config /nonexistent/path/archangel.conf
[ "$status" -eq 1 ]
[[ "$output" == *"Config file not found"* ]]
}
@test "load_config parses a minimal config and sets UNATTENDED" {
local tmp
tmp=$(mktemp)
cat >"$tmp" <<'EOF'
HOSTNAME=testhost
TIMEZONE=UTC
DISKS=/dev/sda,/dev/sdb
ROOT_PASSWORD=secret
EOF
load_config "$tmp"
[ "$HOSTNAME" = "testhost" ]
[ "$TIMEZONE" = "UTC" ]
[ "$ROOT_PASSWORD" = "secret" ]
[ "${SELECTED_DISKS[0]}" = "/dev/sda" ]
[ "${SELECTED_DISKS[1]}" = "/dev/sdb" ]
[ "$UNATTENDED" = "true" ]
rm -f "$tmp"
}
@test "load_config parses a single-disk config into 1-element array" {
local tmp
tmp=$(mktemp)
echo "DISKS=/dev/nvme0n1" >"$tmp"
load_config "$tmp"
[ "${#SELECTED_DISKS[@]}" -eq 1 ]
[ "${SELECTED_DISKS[0]}" = "/dev/nvme0n1" ]
rm -f "$tmp"
}
@test "validate_config fails and lists every missing required field" {
HOSTNAME=""
TIMEZONE=""
SELECTED_DISKS=()
ROOT_PASSWORD=""
run validate_config
[ "$status" -eq 1 ]
[[ "$output" == *"HOSTNAME not set"* ]]
[[ "$output" == *"TIMEZONE not set"* ]]
[[ "$output" == *"No disks selected"* ]]
[[ "$output" == *"ROOT_PASSWORD not set"* ]]
[[ "$output" == *"4 error"* ]]
}
@test "validate_config under set -e reports every error, not just the first" {
# Reproduces the monolith's call structure: `set -e` is active and
# validate_config is invoked as the final command of an && list. A
# post-increment that returns the pre-increment value (0 on the first
# error) trips set -e and aborts the function after one warning. This
# test runs outside bats' `run` shield (which sets +e) so the real
# accumulate-and-report behavior is exercised.
run bash -c '
set -e
source "'"${BATS_TEST_DIRNAME}"'/../../installer/lib/common.sh"
source "'"${BATS_TEST_DIRNAME}"'/../../installer/lib/raid.sh"
source "'"${BATS_TEST_DIRNAME}"'/../../installer/lib/config.sh"
HOSTNAME=""; TIMEZONE=""; SELECTED_DISKS=(); ROOT_PASSWORD=""
UNATTENDED=true
[[ "$UNATTENDED" == true ]] && validate_config
'
[ "$status" -eq 1 ]
[[ "$output" == *"HOSTNAME not set"* ]]
[[ "$output" == *"TIMEZONE not set"* ]]
[[ "$output" == *"No disks selected"* ]]
[[ "$output" == *"ROOT_PASSWORD not set"* ]]
[[ "$output" == *"4 error"* ]]
}
@test "validate_config rejects a RAID_LEVEL invalid for the disk count" {
HOSTNAME=h
TIMEZONE=UTC
ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda /dev/sdb)
RAID_LEVEL=raidz1
run validate_config
[ "$status" -eq 1 ]
[[ "$output" == *"Invalid RAID_LEVEL"* ]]
[[ "$output" == *"raidz1"* ]]
}
@test "validate_config accepts a RAID_LEVEL valid for the disk count" {
HOSTNAME=h
TIMEZONE=UTC
ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda /dev/sdb /dev/sdc)
RAID_LEVEL=raidz1
run validate_config
[[ "$output" != *"Invalid RAID_LEVEL"* ]]
}
@test "validate_config accepts an empty RAID_LEVEL for a single disk" {
HOSTNAME=h
TIMEZONE=UTC
ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda)
RAID_LEVEL=""
run validate_config
[[ "$output" != *"Invalid RAID_LEVEL"* ]]
}
@test "validate_config rejects an invalid timezone" {
HOSTNAME="h"
TIMEZONE="Not/A_Real_Zone_xyz"
SELECTED_DISKS=()
ROOT_PASSWORD="x"
run validate_config
[ "$status" -eq 1 ]
[[ "$output" == *"Invalid timezone"* ]]
}
@test "check_config is a no-op when CONFIG_FILE is unset" {
CONFIG_FILE=""
run check_config
[ "$status" -eq 0 ]
[ -z "$output" ]
}
@test "check_config loads the config file when CONFIG_FILE is set" {
local tmp
tmp=$(mktemp)
cat >"$tmp" <<'EOF'
HOSTNAME=fromcheckconfig
TIMEZONE=UTC
EOF
CONFIG_FILE="$tmp"
check_config
[ "$HOSTNAME" = "fromcheckconfig" ]
[ "$TIMEZONE" = "UTC" ]
[ "$UNATTENDED" = "true" ]
rm -f "$tmp"
}
@test "validate_config flags an existing-but-not-block path in SELECTED_DISKS" {
HOSTNAME=h
TIMEZONE=UTC
ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/null)
run validate_config
[ "$status" -eq 1 ]
[[ "$output" == *"Disk not found: /dev/null"* ]]
}
@test "validate_config flags a missing path in SELECTED_DISKS" {
HOSTNAME=h
TIMEZONE=UTC
ROOT_PASSWORD=x
SELECTED_DISKS=(/nonexistent/disk-xyz-42)
run validate_config
[ "$status" -eq 1 ]
[[ "$output" == *"Disk not found"* ]]
}
@test "parse_args accepts --color and --config-file together (color first)" {
parse_args --color --config-file /tmp/foo.conf
[ "$CONFIG_FILE" = "/tmp/foo.conf" ]
[ -n "$RED" ]
}
@test "parse_args accepts --config-file and --color together (config first)" {
parse_args --config-file /tmp/foo.conf --color
[ "$CONFIG_FILE" = "/tmp/foo.conf" ]
[ -n "$RED" ]
}
#############################
# Default values sourced from config.sh
#############################
# config.sh is the single source of truth for installer defaults. The
# monolith no longer re-applies them in gather_input. These tests pin
# the values so a regression that drops a default surfaces here, not
# halfway through an unattended install.
@test "config.sh sets defaults for FILESYSTEM, LOCALE, KEYMAP, ENABLE_SSH, NO_ENCRYPT" {
[ "$FILESYSTEM" = "zfs" ]
[ "$LOCALE" = "en_US.UTF-8" ]
[ "$KEYMAP" = "us" ]
[ "$ENABLE_SSH" = "yes" ]
[ "$NO_ENCRYPT" = "no" ]
}
#############################
# validate_filesystem
#############################
# Called from main() between check_config and gather_input. Catches a
# typo in FILESYSTEM= from a config file before the install starts.
@test "validate_filesystem accepts zfs" {
FILESYSTEM=zfs
run validate_filesystem
[ "$status" -eq 0 ]
}
@test "validate_filesystem accepts btrfs" {
FILESYSTEM=btrfs
run validate_filesystem
[ "$status" -eq 0 ]
}
@test "validate_filesystem rejects an unknown filesystem" {
FILESYSTEM=ext4
run validate_filesystem
[ "$status" -eq 1 ]
[[ "$output" == *"Invalid FILESYSTEM"* ]]
[[ "$output" == *"ext4"* ]]
}
@test "validate_filesystem rejects an empty FILESYSTEM" {
FILESYSTEM=""
run validate_filesystem
[ "$status" -eq 1 ]
[[ "$output" == *"Invalid FILESYSTEM"* ]]
}
#############################
# validate_encryption_passphrase
#############################
# Called from gather_input's unattended branch. Errors when encryption
# is enabled (NO_ENCRYPT != "yes") but the named passphrase variable
# is empty. Indirect expansion lets one helper cover both ZFS and Btrfs.
@test "validate_encryption_passphrase passes when NO_ENCRYPT=yes regardless of passphrase" {
NO_ENCRYPT=yes
ZFS_PASSPHRASE=""
run validate_encryption_passphrase ZFS_PASSPHRASE
[ "$status" -eq 0 ]
}
@test "validate_encryption_passphrase errors when NO_ENCRYPT=no and passphrase empty" {
NO_ENCRYPT=no
ZFS_PASSPHRASE=""
run validate_encryption_passphrase ZFS_PASSPHRASE
[ "$status" -eq 1 ]
[[ "$output" == *"ZFS_PASSPHRASE"* ]]
[[ "$output" == *"NO_ENCRYPT=yes"* ]]
}
@test "validate_encryption_passphrase passes when NO_ENCRYPT=no and passphrase set" {
NO_ENCRYPT=no
LUKS_PASSPHRASE="hunter2hunter2"
run validate_encryption_passphrase LUKS_PASSPHRASE
[ "$status" -eq 0 ]
}
@test "validate_encryption_passphrase names the offending variable in the error" {
NO_ENCRYPT=no
LUKS_PASSPHRASE=""
run validate_encryption_passphrase LUKS_PASSPHRASE
[ "$status" -eq 1 ]
[[ "$output" == *"LUKS_PASSPHRASE"* ]]
! [[ "$output" == *"ZFS_PASSPHRASE"* ]]
}
@test "validate_encryption_passphrase rejects a passphrase under the minimum length" {
NO_ENCRYPT=no
ZFS_PASSPHRASE="welcome"
run validate_encryption_passphrase ZFS_PASSPHRASE 8
[ "$status" -eq 1 ]
[[ "$output" == *"at least 8"* ]]
[[ "$output" == *"ZFS_PASSPHRASE"* ]]
}
@test "validate_encryption_passphrase accepts a passphrase at exactly the minimum length" {
NO_ENCRYPT=no
ZFS_PASSPHRASE="welcome1"
run validate_encryption_passphrase ZFS_PASSPHRASE 8
[ "$status" -eq 0 ]
}
@test "validate_encryption_passphrase without a minimum keeps the empty-only check" {
NO_ENCRYPT=no
LUKS_PASSPHRASE="hunter2"
run validate_encryption_passphrase LUKS_PASSPHRASE
[ "$status" -eq 0 ]
}
@test "validate_encryption_passphrase skips the length check when NO_ENCRYPT=yes" {
NO_ENCRYPT=yes
ZFS_PASSPHRASE="short"
run validate_encryption_passphrase ZFS_PASSPHRASE 8
[ "$status" -eq 0 ]
}
@test "validate_encryption_passphrase rejects a passphrase over the maximum length" {
NO_ENCRYPT=no
ZFS_PASSPHRASE=$(printf 'a%.0s' {1..513})
run validate_encryption_passphrase ZFS_PASSPHRASE 8 512
[ "$status" -eq 1 ]
[[ "$output" == *"at most 512"* ]]
}
@test "validate_encryption_passphrase accepts a passphrase at exactly the maximum length" {
NO_ENCRYPT=no
ZFS_PASSPHRASE=$(printf 'a%.0s' {1..512})
run validate_encryption_passphrase ZFS_PASSPHRASE 8 512
[ "$status" -eq 0 ]
}
#############################
# SWAP_SIZE validation
#############################
@test "validate_config accepts empty SWAP_SIZE (default layout)" {
HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda); RAID_LEVEL=""; SWAP_SIZE=""
run validate_config
[[ "$output" != *"SWAP_SIZE"* ]]
}
@test "validate_config accepts a well-formed SWAP_SIZE on a single disk" {
HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda); RAID_LEVEL=""; SWAP_SIZE=100G
run validate_config
[[ "$output" != *"Invalid SWAP_SIZE"* ]]
[[ "$output" != *"single-disk"* ]]
}
@test "validate_config rejects SWAP_SIZE without a unit suffix" {
HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda); RAID_LEVEL=""; SWAP_SIZE=100
run validate_config
[ "$status" -eq 1 ]
[[ "$output" == *"Invalid SWAP_SIZE"* ]]
}
@test "validate_config rejects SWAP_SIZE with a bogus unit" {
HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda); RAID_LEVEL=""; SWAP_SIZE=100Q
run validate_config
[ "$status" -eq 1 ]
[[ "$output" == *"Invalid SWAP_SIZE"* ]]
}
@test "validate_config rejects a zero SWAP_SIZE" {
HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda); RAID_LEVEL=""; SWAP_SIZE=0G
run validate_config
[ "$status" -eq 1 ]
[[ "$output" == *"Invalid SWAP_SIZE"* ]]
}
@test "validate_config rejects a leading-zero SWAP_SIZE" {
HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda); RAID_LEVEL=""; SWAP_SIZE=00G
run validate_config
[ "$status" -eq 1 ]
[[ "$output" == *"Invalid SWAP_SIZE"* ]]
}
@test "validate_config warns that swap is unencrypted when encryption is on" {
HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda); RAID_LEVEL=""; SWAP_SIZE=100G; NO_ENCRYPT=no
run validate_config
[[ "$output" == *"unencrypted"* ]]
}
@test "validate_config does not warn about swap encryption when NO_ENCRYPT=yes" {
HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda); RAID_LEVEL=""; SWAP_SIZE=100G; NO_ENCRYPT=yes
run validate_config
[[ "$output" != *"unencrypted"* ]]
}
@test "validate_config rejects SWAP_SIZE on a multi-disk layout" {
HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda /dev/sdb); RAID_LEVEL=mirror; SWAP_SIZE=100G
run validate_config
[ "$status" -eq 1 ]
[[ "$output" == *"single-disk"* ]]
}
|