Skip to content

Repository files, collaboration, releases and project activity.

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