160 lines
6.8 KiB
Python
160 lines
6.8 KiB
Python
"""SD provisioning: ENV.TXT (firmware-exact parsing), disk discovery parsers,
|
|
the elevated Windows format script, and write/check on a stand-in card."""
|
|
|
|
import plistlib
|
|
|
|
import pytest
|
|
|
|
from kota_tech.sd.disks import MAX_CARD_BYTES, Disk, parse_macos, parse_windows
|
|
from kota_tech.sd.envfile import EnvConfig, parse_env
|
|
from kota_tech.sd.provision import check_card, windows_format_script, write_env
|
|
|
|
URL = "https://influx.kota-app.com/api/v2/write?bucket=sensorbox&org=kota"
|
|
TOKEN = "T" * 86 + "==" # shape of an InfluxDB token, not a real one
|
|
|
|
|
|
def cfg(**kw):
|
|
base = dict(influx_url=URL, influx_token=TOKEN)
|
|
base.update(kw)
|
|
return EnvConfig(**base)
|
|
|
|
|
|
# ------------------------------------------------------------------ ENV.TXT
|
|
|
|
def test_render_matches_the_factory_file_format():
|
|
raw = cfg().render()
|
|
assert raw == f"INFLUX_TOKEN={TOKEN}\nINFLUX_URL={URL}\n".encode()
|
|
assert not raw.startswith(b"\xef\xbb\xbf") and b"\r" not in raw
|
|
assert parse_env(raw).ok
|
|
|
|
|
|
def test_wifi_only_written_when_both_set():
|
|
raw = cfg(ssid="Site WiFi", password="correct horse").render().decode()
|
|
assert "SSID=Site WiFi\n" in raw and "PASSWORD=correct horse\n" in raw
|
|
assert any("both SSID and PASSWORD" in i.text for i in cfg(ssid="x").problems())
|
|
|
|
|
|
@pytest.mark.parametrize("kw,fragment", [
|
|
(dict(influx_url="influx.kota-app.com"), "https://"),
|
|
(dict(influx_url="https://influx.kota-app.com/"), "write URL"),
|
|
(dict(influx_token=""), "INFLUX_TOKEN is empty"),
|
|
(dict(influx_token="abc def" * 5), "spaces"),
|
|
(dict(influx_token=TOKEN + " "), "space"),
|
|
(dict(ssid="s" * 33, password="12345678"), "32 bytes"),
|
|
(dict(ssid="net", password="short"), "8-63"),
|
|
])
|
|
def test_invalid_settings_are_errors(kw, fragment):
|
|
errors = [i.text for i in cfg(**kw).problems() if i.level == "error"]
|
|
assert any(fragment in e for e in errors), errors
|
|
|
|
|
|
def test_parser_mirrors_firmware():
|
|
raw = b"\xef\xbb\xbfINFLUX_TOKEN=abc\r\nINFLUX_URL = x\nINFLUX_URL=u1\nINFLUX_URL=u2\nHTTP_SERVER=true\n"
|
|
chk = parse_env(raw)
|
|
texts = " ".join(i.text for i in chk.issues)
|
|
assert "BOM" in texts # first key unreadable
|
|
assert "spaces around the key 'INFLUX_URL'" in texts
|
|
assert chk.values["INFLUX_URL"] == "u1" # first match wins
|
|
assert "appears twice" in texts and "setup mode" in texts
|
|
assert "INFLUX_TOKEN is missing" in texts # hidden behind the BOM
|
|
assert not chk.ok
|
|
|
|
|
|
def test_masking():
|
|
chk = parse_env(cfg(ssid="net", password="secretpassword").render())
|
|
m = chk.masked()
|
|
assert TOKEN not in m["INFLUX_TOKEN"] and m["INFLUX_TOKEN"].startswith("TTTT")
|
|
assert "secret" not in m["PASSWORD"]
|
|
|
|
|
|
# ------------------------------------------------------------- disk lists
|
|
|
|
WIN_JSON = """[{"Number":0,"FriendlyName":"ST500DM002-1BC142","Size":500107862016,"BusType":"SATA",
|
|
"Volumes":[{"DriveLetter":"D","FileSystem":"NTFS","Label":"WINRE"}]},
|
|
{"Number":4,"FriendlyName":"Mass Storage Device","Size":0,"BusType":"USB","Volumes":[]},
|
|
{"Number":5,"FriendlyName":"SDXC Card","Size":31914983424,"BusType":"SD",
|
|
"Volumes":{"DriveLetter":"E","FileSystem":"FAT32","Label":"KOTA"}}]"""
|
|
|
|
|
|
def test_parse_windows_keeps_only_card_buses():
|
|
disks = parse_windows(WIN_JSON)
|
|
assert [d.id for d in disks] == ["4", "5"] # SATA data disk never listed
|
|
reader, card = disks
|
|
assert not reader.has_media and reader.size_text == "no card"
|
|
assert card.volumes[0].mount == "E:\\" and card.volumes[0].filesystem == "FAT32"
|
|
|
|
|
|
def test_parse_windows_single_object_quirk():
|
|
one = '{"Number":5,"FriendlyName":"SD","Size":8000000000,"BusType":"USB","Volumes":null}'
|
|
assert [d.id for d in parse_windows(one)] == ["5"]
|
|
|
|
|
|
def test_parse_macos_excludes_system_disk():
|
|
listing = plistlib.dumps({"AllDisksAndPartitions": [
|
|
{"DeviceIdentifier": "disk0", "Size": 500_000_000_000, "Partitions": []},
|
|
{"DeviceIdentifier": "disk3", "Size": 994_000_000_000, "Partitions": [], "APFSVolumes": [{"MountPoint": "/"}]},
|
|
{"DeviceIdentifier": "disk4", "Size": 15_931_539_456,
|
|
"Partitions": [{"DeviceIdentifier": "disk4s1", "MountPoint": "/Volumes/KOTA", "Content": "DOS_FAT_32",
|
|
"VolumeName": "KOTA"}]},
|
|
]})
|
|
infos = {"disk0": plistlib.dumps({"Internal": True, "BusProtocol": "PCI-Express"}),
|
|
"disk3": plistlib.dumps({"Internal": True}),
|
|
"disk4": plistlib.dumps({"Internal": True, "RemovableMedia": True, "BusProtocol": "Secure Digital",
|
|
"MediaName": "SD Card Reader"})}
|
|
disks = parse_macos(listing, infos)
|
|
assert [d.id for d in disks] == ["/dev/disk4"] and disks[0].bus == "SD"
|
|
assert disks[0].volumes[0].mount == "/Volumes/KOTA"
|
|
|
|
|
|
# -------------------------------------------------------- format script
|
|
|
|
def test_windows_script_rechecks_disk_before_erasing(tmp_path):
|
|
disk = Disk("5", "SDXC Card", 63_864_569_856, "SD")
|
|
ps = windows_format_script(disk, tmp_path / "r.json")
|
|
erase = ps.index("Clear-Disk")
|
|
for guard in ("IsSystem -or $d.IsBoot", "'MMC','SD','USB'", "-ne 63864569856"):
|
|
assert guard in ps and ps.index(guard) < erase
|
|
assert "-PartitionStyle MBR" in ps and "-FileSystem FAT32" in ps
|
|
assert str(32 * 1024**3) in ps # FAT32 partition capped at 32 GB
|
|
assert "NewFileSystemLabel 'KOTA'" in ps
|
|
|
|
|
|
def test_large_disks_are_not_cards():
|
|
assert MAX_CARD_BYTES == 256 * 1024**3
|
|
|
|
|
|
# ----------------------------------------------- write / check on a card
|
|
|
|
def test_write_env_replaces_variants_and_keeps_buffered_data(tmp_path):
|
|
card = tmp_path / "card"
|
|
card.mkdir()
|
|
(card / "env.txt").write_bytes(b"INFLUX_URL=old\n")
|
|
(card / "BUFFER.TXT").write_bytes(b"line1\nline2\n\n")
|
|
out = write_env(str(card), cfg())
|
|
assert out.ok, out.message
|
|
names = sorted(p.name for p in card.iterdir())
|
|
assert names == ["BUFFER.TXT", "ENV.TXT"]
|
|
rep = check_card(str(card), "FAT32")
|
|
assert rep.env.ok and rep.problems == [] and rep.buffered_lines == 2
|
|
|
|
|
|
def test_write_env_refuses_invalid_settings(tmp_path):
|
|
assert not write_env(str(tmp_path), cfg(influx_token="")).ok
|
|
assert not (tmp_path / "ENV.TXT").exists()
|
|
|
|
|
|
def test_check_card_reports_missing_env_and_wrong_fs(tmp_path):
|
|
rep = check_card(str(tmp_path), "exFAT")
|
|
texts = " ".join(rep.problems)
|
|
assert "needs FAT32" in texts and "No ENV.TXT" in texts
|
|
|
|
|
|
def test_elevated_result_folder_is_not_owner_only(tmp_path, monkeypatch):
|
|
# Regression: tempfile.mkdtemp() made the elevated script's result unreadable.
|
|
import kota_tech.sd.provision as prov
|
|
monkeypatch.setattr(prov, "WORK_ROOT", tmp_path / "tmp")
|
|
work = prov._workdir()
|
|
assert work.parent == tmp_path / "tmp" and work.is_dir()
|
|
(work / "result.json").write_text('{"ok": true}', encoding="utf-8")
|
|
assert (work / "result.json").read_text(encoding="utf-8")
|