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 python32
"""Real PTY smoke-test of interactive admin creation, including password secrecy."""3
import base64, errno, hashlib, hmac, os, pty, re, select, shutil, struct, subprocess, sys, tempfile, time5
APP_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))6
NODE = sys.argv[1] if len(sys.argv)>1 else shutil.which('node')7
assert NODE, 'Node.js executable not found'10
def 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()+timeout23
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: break31
raise32
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]&1552
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==065
assert 'An admin already exists' in result.stdout66
print('PASS: TTY admin creation, password not echoed, TOTP and existing-account protection')68
if __name__=='__main__': test_admin_setup()