Skip to content

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-111 #!/usr/bin/env python3
f1897d33c0archadmin2026-10-112 """Real PTY smoke-test of interactive admin creation, including password secrecy."""
f1897d33c0archadmin2026-10-113 import base64, errno, hashlib, hmac, os, pty, re, select, shutil, struct, subprocess, sys, tempfile, time
f1897d33c0archadmin2026-10-114 
f1897d33c0archadmin2026-10-115 APP_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
f1897d33c0archadmin2026-10-116 NODE = sys.argv[1] if len(sys.argv)>1 else shutil.which('node')
f1897d33c0archadmin2026-10-117 assert NODE, 'Node.js executable not found'
f1897d33c0archadmin2026-10-118 
f1897d33c0archadmin2026-10-119 
f1897d33c0archadmin2026-10-1110 def test_admin_setup():
f1897d33c0archadmin2026-10-1111  with tempfile.TemporaryDirectory(prefix='archforge-admin-pty-') as directory:
f1897d33c0archadmin2026-10-1112  dbfile = os.path.join(directory,'status.db')
f1897d33c0archadmin2026-10-1113  env = dict(os.environ, STATUS_DB_PATH=dbfile, STATUS_ENCRYPTION_KEY='a'*64,
f1897d33c0archadmin2026-10-1114  STATUS_ENV_FILE=os.path.join(directory,'nonexistent-env'), NODE_ENV='production', NODE_NO_WARNINGS='1')
f1897d33c0archadmin2026-10-1115  master, slave = pty.openpty()
f1897d33c0archadmin2026-10-1116  proc = subprocess.Popen([NODE,'src/cli.js','create-admin'],
f1897d33c0archadmin2026-10-1117  stdin=slave, stdout=slave, stderr=slave,
f1897d33c0archadmin2026-10-1118  cwd=APP_ROOT, env=env, start_new_session=True)
f1897d33c0archadmin2026-10-1119  os.close(slave)
f1897d33c0archadmin2026-10-1120  captured = bytearray()
f1897d33c0archadmin2026-10-1121  def until(token, timeout=6):
f1897d33c0archadmin2026-10-1122  deadline=time.monotonic()+timeout
f1897d33c0archadmin2026-10-1123  while token not in captured:
f1897d33c0archadmin2026-10-1124  remaining=deadline-time.monotonic()
f1897d33c0archadmin2026-10-1125  if remaining<=0: raise AssertionError(f'timed out waiting for {token!r}: {captured[-600:]!r}')
f1897d33c0archadmin2026-10-1126  readable,_,_=select.select([master],[],[],remaining)
f1897d33c0archadmin2026-10-1127  if readable:
f1897d33c0archadmin2026-10-1128  try: fragment=os.read(master,8192)
f1897d33c0archadmin2026-10-1129  except OSError as exc:
f1897d33c0archadmin2026-10-1130  if exc.errno==errno.EIO: break
f1897d33c0archadmin2026-10-1131  raise
f1897d33c0archadmin2026-10-1132  captured.extend(fragment)
f1897d33c0archadmin2026-10-1133  assert token in captured, f'missing {token!r}: {captured[-600:]!r}'
f1897d33c0archadmin2026-10-1134  try:
f1897d33c0archadmin2026-10-1135  until(b'Admin username')
f1897d33c0archadmin2026-10-1136  os.write(master,b'archadmin\r')
f1897d33c0archadmin2026-10-1137  until(b'New admin password')
f1897d33c0archadmin2026-10-1138  secret_password=b'UniqueSecurePassword159!'
f1897d33c0archadmin2026-10-1139  os.write(master,secret_password+b'\r')
f1897d33c0archadmin2026-10-1140  until(b'Confirm password')
f1897d33c0archadmin2026-10-1141  assert secret_password not in captured, 'password leaked to terminal output'
f1897d33c0archadmin2026-10-1142  os.write(master,secret_password+b'\r')
f1897d33c0archadmin2026-10-1143  until(b'TOTP SECRET:')
f1897d33c0archadmin2026-10-1144  until(b'Enter the current 6-digit code')
f1897d33c0archadmin2026-10-1145  assert secret_password not in captured, 'password leaked to terminal output'
f1897d33c0archadmin2026-10-1146  match=re.search(rb'TOTP SECRET:\s*([A-Z2-7]+)', bytes(captured))
f1897d33c0archadmin2026-10-1147  assert match, 'authenticator secret missing from output'
f1897d33c0archadmin2026-10-1148  key=base64.b32decode(match.group(1)+b'='*((8-len(match.group(1))%8)%8))
f1897d33c0archadmin2026-10-1149  epoch=int(time.time()//30)
f1897d33c0archadmin2026-10-1150  digest=hmac.new(key,struct.pack('>Q',epoch),hashlib.sha1).digest()
f1897d33c0archadmin2026-10-1151  offset=digest[-1]&15
f1897d33c0archadmin2026-10-1152  code=str((struct.unpack('>I',digest[offset:offset+4])[0]&0x7fffffff)%1000000).zfill(6)
f1897d33c0archadmin2026-10-1153  os.write(master,code.encode()+b'\r')
f1897d33c0archadmin2026-10-1154  until(b'Admin created successfully')
f1897d33c0archadmin2026-10-1155  proc.wait(timeout=6)
f1897d33c0archadmin2026-10-1156  assert proc.returncode==0, f'admin setup exit code: {proc.returncode}'
f1897d33c0archadmin2026-10-1157  assert secret_password not in captured, 'password leaked after completion'
f1897d33c0archadmin2026-10-1158  finally:
f1897d33c0archadmin2026-10-1159  if proc.poll() is None: proc.kill();proc.wait()
f1897d33c0archadmin2026-10-1160  os.close(master)
f1897d33c0archadmin2026-10-1161  # Verify an existing admin isn't replaced by another setup invocation.
f1897d33c0archadmin2026-10-1162  result=subprocess.run([NODE,'src/cli.js','create-admin'], cwd=APP_ROOT, env=env,
f1897d33c0archadmin2026-10-1163  capture_output=True,text=True,timeout=6)
f1897d33c0archadmin2026-10-1164  assert result.returncode==0
f1897d33c0archadmin2026-10-1165  assert 'An admin already exists' in result.stdout
f1897d33c0archadmin2026-10-1166  print('PASS: TTY admin creation, password not echoed, TOTP and existing-account protection')
f1897d33c0archadmin2026-10-1167 
f1897d33c0archadmin2026-10-1168 if __name__=='__main__': test_admin_setup()