Files
felhom.eu/scripts/test_read_credential.py
T

172 lines
7.3 KiB
Python

# -*- coding: utf-8 -*-
"""Fixture tests for read_credential.py.
Run: python3 scripts/test_read_credential.py
Every test asserts the EFFECT — the refusal happens, and the message names the reason — not merely
that the function ran. Fixtures are temp files; nothing here reads the real credentials file, and no
test contains a real secret.
"""
import os
import subprocess
import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import read_credential as rc # noqa: E402
FAILURES = []
def check(name, cond, detail=""):
if cond:
print(" OK %s" % name)
else:
print(" FAIL %s %s" % (name, detail))
FAILURES.append(name)
def writefile(body):
fd, path = tempfile.mkstemp()
with os.fdopen(fd, "w", encoding="utf-8") as fh:
fh.write(body)
return path
# --- E1 — TestR404_CredentialLengthMismatchFailsLoudly ------------------------------------------
#
# THE REGRESSION THIS PINS, stated as the thing that actually happened: a session stripped only `"`
# from a single-quoted value, sent 15 characters where the password is 13, read the resulting
# 200-with-login-page as "the password drifted", and rewrote a live box's password hash.
#
# RED-PROOF (recorded in REPORT.md): delete the final quote-character assertion in `unwrap` and this
# test fails on `quote survives a one-sided strip`.
def test_r404_credential_length_mismatch_fails_loudly():
print("E1 TestR404_CredentialLengthMismatchFailsLoudly")
# The exact 2026-08-31 shape: single-quoted, and only `"` was stripped by the caller. The reader
# must never hand back a value carrying a quote.
p = writefile("PASSWORD='abcdefghijklm'\n")
check("single-quoted value unwraps to its 13 characters",
rc.read(p, "PASSWORD") == "abcdefghijklm")
os.unlink(p)
# A value that still carries a quote must be REFUSED, not returned.
try:
rc.unwrap("'abcdefghijklm")
check("quote survives a one-sided strip", False, "no refusal was raised")
except rc.CredentialError as exc:
check("quote survives a one-sided strip", "one side only" in str(exc), str(exc))
try:
rc.unwrap("abcdefghijklm'")
check("trailing-only quote is refused", False, "no refusal was raised")
except rc.CredentialError as exc:
check("trailing-only quote is refused", "one side only" in str(exc), str(exc))
# An unquoted value is legitimate and passes through untouched.
check("unquoted value passes through", rc.unwrap("abcdefghijklm") == "abcdefghijklm")
# Mismatched quote characters are not a pair.
try:
rc.unwrap("'abcdefghijklm\"")
check("mismatched quote pair is refused", False, "no refusal was raised")
except rc.CredentialError as exc:
check("mismatched quote pair is refused", "one side only" in str(exc), str(exc))
# Empty is refused — an empty password authenticates as nothing and reads as a wrong password.
try:
rc.unwrap("''")
check("empty value is refused", False, "no refusal was raised")
except rc.CredentialError as exc:
check("empty value is refused", "empty" in str(exc), str(exc))
# The caller's second opinion: --expect-length refuses a value of the wrong size BEFORE use.
p = writefile("PASSWORD='abcdefghijklm'\n")
out = tempfile.mkstemp()[1]
rcode = rc.main([ "PASSWORD", out, "--credentials", p, "--expect-length", "15" ])
check("--expect-length 15 is REFUSED for a 13-character value", rcode == 2, "rc=%s" % rcode)
rcode = rc.main([ "PASSWORD", out, "--credentials", p, "--expect-length", "13" ])
check("--expect-length 13 is accepted", rcode == 0, "rc=%s" % rcode)
with open(out, encoding="utf-8") as fh:
check("the value reached the file", fh.read() == "abcdefghijklm")
check("the file is 0600", oct(os.stat(out).st_mode & 0o777) == "0o600")
os.unlink(p)
os.unlink(out)
# A missing key is a refusal, not an empty string.
p = writefile("OTHER='x'\n")
try:
rc.read(p, "PASSWORD")
check("missing key is refused", False, "no refusal was raised")
except rc.CredentialError as exc:
check("missing key is refused", "not present" in str(exc), str(exc))
os.unlink(p)
# --- the value must never reach stdout ------------------------------------------------------------
def test_the_value_is_never_printed():
print("TestR404_TheValueIsNeverPrinted")
p = writefile("PASSWORD='swordfish1234'\n")
out = tempfile.mkstemp()[1]
res = subprocess.run(
[sys.executable, os.path.join(os.path.dirname(os.path.abspath(__file__)), "read_credential.py"),
"PASSWORD", out, "--credentials", p],
capture_output=True, text=True)
combined = res.stdout + res.stderr
check("exit 0", res.returncode == 0, combined)
# POSITIVE CONTROL first: the grep can find the secret when it IS there. A "not found" from a
# search that cannot find anything is not a measurement.
check("positive control — the search finds a planted copy",
"swordfish1234" in (combined + "swordfish1234"))
check("the secret is NOT in stdout/stderr", "swordfish1234" not in combined, combined)
check("the length IS reported", "13 characters" in res.stdout, res.stdout)
os.unlink(p)
os.unlink(out)
# --- the outfile can never be the credentials file -------------------------------------------------
#
# THE INCIDENT THIS PINS (2026-10-15 night, CC on DooPlex): `read_credential.py HUB_TOKEN ~/.config/credentials`
# — the credentials path given as the OUTFILE — truncated the operator's credentials file to one value.
# RED-PROOF (recorded in the app-guides night's evidence): delete the samefile guard in `main` and the three
# „unchanged" checks below fail.
def test_the_outfile_can_never_be_the_credentials_file():
print("TestCredentialsFileIsNeverTheOutfile")
body = "PASSWORD='abcdefghijklm'\nOTHER='kept'\n"
p = writefile(body)
rcode = rc.main(["PASSWORD", p, "--credentials", p])
with open(p, encoding="utf-8") as fh:
after = fh.read()
check("same path is REFUSED", rcode == 2, "rc=%s" % rcode)
check("same path: the credentials file is unchanged", after == body, repr(after))
link = p + ".link"
os.symlink(p, link)
rcode = rc.main(["PASSWORD", link, "--credentials", p])
with open(p, encoding="utf-8") as fh:
after = fh.read()
check("a symlink to it is REFUSED", rcode == 2, "rc=%s" % rcode)
check("symlink: the credentials file is unchanged", after == body, repr(after))
hard = p + ".hard"
os.link(p, hard)
rcode = rc.main(["PASSWORD", hard, "--credentials", p])
with open(p, encoding="utf-8") as fh:
after = fh.read()
check("a hard link to it is REFUSED", rcode == 2, "rc=%s" % rcode)
check("hard link: the credentials file is unchanged", after == body, repr(after))
# Negative control: a real temp outfile still works.
out = tempfile.mkstemp()[1]
check("a separate outfile is still accepted", rc.main(["PASSWORD", out, "--credentials", p]) == 0)
for f in (p, link, hard, out):
os.unlink(f)
if __name__ == "__main__":
test_r404_credential_length_mismatch_fails_loudly()
test_the_value_is_never_printed()
test_the_outfile_can_never_be_the_credentials_file()
if FAILURES:
print("\nFAILED: %d" % len(FAILURES))
sys.exit(1)
print("\nread_credential tests OK")