Repository files, collaboration, releases and project activity.
ARCHFORGE HISTORY
Blame · tests/admin-wizard-pty.py
Branch main · the Commit author is not necessarily a verified ArchForge account.
f1897d33c0archadmin2026-10-11
1 #!/usr/bin/env python3f1897d33c0archadmin2026-10-11
2 """Real PTY smoke-test of interactive admin creation, including password secrecy."""f1897d33c0archadmin2026-10-11
3 import base64, errno, hashlib, hmac, os, pty, re, select, shutil, struct, subprocess, sys, tempfile, timef1897d33c0archadmin2026-10-11
4 f1897d33c0archadmin2026-10-11
5 APP_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))f1897d33c0archadmin2026-10-11
6 NODE = sys.argv[1] if len(sys.argv)>1 else shutil.which('node')f1897d33c0archadmin2026-10-11
7 assert NODE, 'Node.js executable not found'f1897d33c0archadmin2026-10-11
8 f1897d33c0archadmin2026-10-11
9 f1897d33c0archadmin2026-10-11
10 def test_admin_setup():f1897d33c0archadmin2026-10-11
11 with tempfile.TemporaryDirectory(prefix='archforge-admin-pty-') as directory:f1897d33c0archadmin2026-10-11
12 dbfile = os.path.join(directory,'status.db')f1897d33c0archadmin2026-10-11
13 env = dict(os.environ, STATUS_DB_PATH=dbfile, STATUS_ENCRYPTION_KEY='a'*64,f1897d33c0archadmin2026-10-11
14 STATUS_ENV_FILE=os.path.join(directory,'nonexistent-env'), NODE_ENV='production', NODE_NO_WARNINGS='1')f1897d33c0archadmin2026-10-11
15 master, slave = pty.openpty()f1897d33c0archadmin2026-10-11
16 proc = subprocess.Popen([NODE,'src/cli.js','create-admin'],f1897d33c0archadmin2026-10-11
17 stdin=slave, stdout=slave, stderr=slave,f1897d33c0archadmin2026-10-11
18 cwd=APP_ROOT, env=env, start_new_session=True)f1897d33c0archadmin2026-10-11
19 os.close(slave)f1897d33c0archadmin2026-10-11
20 captured = bytearray()f1897d33c0archadmin2026-10-11
21 def until(token, timeout=6):f1897d33c0archadmin2026-10-11
22 deadline=time.monotonic()+timeoutf1897d33c0archadmin2026-10-11
23 while token not in captured:f1897d33c0archadmin2026-10-11
24 remaining=deadline-time.monotonic()f1897d33c0archadmin2026-10-11
25 if remaining<=0: raise AssertionError(f'timed out waiting for {token!r}: {captured[-600:]!r}')f1897d33c0archadmin2026-10-11
26 readable,_,_=select.select([master],[],[],remaining)f1897d33c0archadmin2026-10-11
27 if readable:f1897d33c0archadmin2026-10-11
28 try: fragment=os.read(master,8192)f1897d33c0archadmin2026-10-11
29 except OSError as exc:f1897d33c0archadmin2026-10-11
30 if exc.errno==errno.EIO: breakf1897d33c0archadmin2026-10-11
31 raisef1897d33c0archadmin2026-10-11
32 captured.extend(fragment)f1897d33c0archadmin2026-10-11
33 assert token in captured, f'missing {token!r}: {captured[-600:]!r}'f1897d33c0archadmin2026-10-11
34 try:f1897d33c0archadmin2026-10-11
35 until(b'Admin username')f1897d33c0archadmin2026-10-11
36 os.write(master,b'archadmin\r')f1897d33c0archadmin2026-10-11
37 until(b'New admin password')f1897d33c0archadmin2026-10-11
38 secret_password=b'UniqueSecurePassword159!'f1897d33c0archadmin2026-10-11
39 os.write(master,secret_password+b'\r')f1897d33c0archadmin2026-10-11
40 until(b'Confirm password')f1897d33c0archadmin2026-10-11
41 assert secret_password not in captured, 'password leaked to terminal output'f1897d33c0archadmin2026-10-11
42 os.write(master,secret_password+b'\r')f1897d33c0archadmin2026-10-11
43 until(b'TOTP SECRET:')f1897d33c0archadmin2026-10-11
44 until(b'Enter the current 6-digit code')f1897d33c0archadmin2026-10-11
45 assert secret_password not in captured, 'password leaked to terminal output'f1897d33c0archadmin2026-10-11
46 match=re.search(rb'TOTP SECRET:\s*([A-Z2-7]+)', bytes(captured))f1897d33c0archadmin2026-10-11
47 assert match, 'authenticator secret missing from output'f1897d33c0archadmin2026-10-11
48 key=base64.b32decode(match.group(1)+b'='*((8-len(match.group(1))%8)%8))f1897d33c0archadmin2026-10-11
49 epoch=int(time.time()//30)f1897d33c0archadmin2026-10-11
50 digest=hmac.new(key,struct.pack('>Q',epoch),hashlib.sha1).digest()f1897d33c0archadmin2026-10-11
51 offset=digest[-1]&15f1897d33c0archadmin2026-10-11
52 code=str((struct.unpack('>I',digest[offset:offset+4])[0]&0x7fffffff)%1000000).zfill(6)f1897d33c0archadmin2026-10-11
53 os.write(master,code.encode()+b'\r')f1897d33c0archadmin2026-10-11
54 until(b'Admin created successfully')f1897d33c0archadmin2026-10-11
55 proc.wait(timeout=6)f1897d33c0archadmin2026-10-11
56 assert proc.returncode==0, f'admin setup exit code: {proc.returncode}'f1897d33c0archadmin2026-10-11
57 assert secret_password not in captured, 'password leaked after completion'f1897d33c0archadmin2026-10-11
58 finally:f1897d33c0archadmin2026-10-11
59 if proc.poll() is None: proc.kill();proc.wait()f1897d33c0archadmin2026-10-11
60 os.close(master)f1897d33c0archadmin2026-10-11
61 # Verify an existing admin isn't replaced by another setup invocation.f1897d33c0archadmin2026-10-11
62 result=subprocess.run([NODE,'src/cli.js','create-admin'], cwd=APP_ROOT, env=env,f1897d33c0archadmin2026-10-11
63 capture_output=True,text=True,timeout=6)f1897d33c0archadmin2026-10-11
64 assert result.returncode==0f1897d33c0archadmin2026-10-11
65 assert 'An admin already exists' in result.stdoutf1897d33c0archadmin2026-10-11
66 print('PASS: TTY admin creation, password not echoed, TOTP and existing-account protection')f1897d33c0archadmin2026-10-11
67 f1897d33c0archadmin2026-10-11
68 if __name__=='__main__': test_admin_setup()