Skip to content

Commit 9b13503

Browse files
eastmadcclaude
andcommitted
test(firmware): end-to-end regression for tar/zip-of-FAT-image
Builds a real Eaton-shape tar and zip (each containing a minimal FAT16 stub + EULA + manifest.json), streams the file through FirmwareService.upload, and asserts: 1. Tar-of-FAT-image → firmware.extracted_path is None. The upload-time shortcut must fall through to the terminal path so the frontend's subsequent POST /unpack hands the file to unblob. 2. Zip-of-FAT-image → firmware.extracted_path is None. Same defect class, same expected fall-through behaviour. 3. Pure-rootfs tar (ADB-dump shape) → firmware.extracted_path is set AND firmware.device_metadata['detection_roots'] is a non-empty list of existing directories (Rule #16 guard). Each test builds its own firmware tarball via tarfile+BytesIO and streams the bytes through a MagicMock UploadFile that replicates FastAPI's UploadFile.read()/size surface — no network, no real DB, fast (<1s total). Settings are patched with per-test storage_root under tmp_path so the tests are hermetic. Together these guard the commit chain: - 38d01d8 (find_filesystem_root fallback gate) — #1 and #2 - 5b8d606 (detection_roots on shortcut paths) — #3 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5b8d606 commit 9b13503

1 file changed

Lines changed: 213 additions & 0 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
"""Integration tests for tar/zip uploads that wrap raw filesystem images.
2+
3+
Guards the Eaton Network M3 regression end-to-end: a vendor tarball
4+
containing ``{EULA, manifest.json, .data_img(FAT16)}`` must NOT be
5+
silently classified as "rootfs" by the upload-time shortcut. It must
6+
fall through to the terminal path so the user's ``POST /unpack`` call
7+
eventually hands the file to unblob, which can extract the FAT16
8+
filesystem.
9+
10+
Strategy:
11+
- Build a minimal FAT16 stub via ``_make_fat16_stub``.
12+
- Pack it into a real ``.tar`` or ``.zip`` alongside text metadata.
13+
- Invoke ``FirmwareService.create_firmware`` with a mock upload.
14+
- Assert the resulting ``Firmware`` row has ``extracted_path=None``
15+
(shortcut fell through) for the image case, AND has it set for
16+
the pure-rootfs-tar control case.
17+
18+
Scenarios:
19+
1. Tar-of-FAT-image (Eaton shape) → no extracted_path.
20+
2. Tar-of-pure-rootfs (ADB-dump shape with etc/, usr/, bin/)
21+
→ extracted_path set AND detection_roots populated (Rule #16).
22+
3. Zip-of-FAT-image (latent defect, parallel of #1) → no extracted_path.
23+
"""
24+
from __future__ import annotations
25+
26+
import io
27+
import os
28+
import tarfile
29+
import uuid
30+
import zipfile
31+
from pathlib import Path
32+
from unittest.mock import AsyncMock, MagicMock, patch
33+
34+
import pytest
35+
36+
from app.services.firmware_service import FirmwareService
37+
38+
39+
def _make_fat16_stub_bytes(size_kb: int = 8) -> bytes:
40+
buf = bytearray(size_kb * 1024)
41+
buf[54:62] = b"FAT16 "
42+
buf[510:512] = b"\x55\xaa"
43+
return bytes(buf)
44+
45+
46+
def _make_rootfs_tar(path: Path) -> None:
47+
"""Build a pure-rootfs tar: etc/ + usr/ + bin/ dirs + a couple files."""
48+
with tarfile.open(path, "w") as tf:
49+
# etc/ with one file
50+
etc_data = b"root:x:0:0::/:/bin/sh\n"
51+
ti = tarfile.TarInfo("etc/passwd")
52+
ti.size = len(etc_data)
53+
tf.addfile(ti, io.BytesIO(etc_data))
54+
# usr/ with one stub file
55+
stub_data = b"#!/bin/sh\n"
56+
ti2 = tarfile.TarInfo("usr/bin/true")
57+
ti2.size = len(stub_data)
58+
ti2.mode = 0o755
59+
tf.addfile(ti2, io.BytesIO(stub_data))
60+
# bin/ stub
61+
ti3 = tarfile.TarInfo("bin/sh")
62+
ti3.size = len(stub_data)
63+
ti3.mode = 0o755
64+
tf.addfile(ti3, io.BytesIO(stub_data))
65+
66+
67+
def _make_tar_of_image(path: Path) -> None:
68+
"""Build an Eaton-shape tar: EULA + manifest.json + .data_img(FAT16)."""
69+
fat_bytes = _make_fat16_stub_bytes()
70+
eula_bytes = b"END USER LICENSE AGREEMENT ... " * 16
71+
manifest_bytes = b'{"vendor": "TestVendor", "version": "1.0.0"}'
72+
73+
with tarfile.open(path, "w") as tf:
74+
ti = tarfile.TarInfo("EULA")
75+
ti.size = len(eula_bytes)
76+
tf.addfile(ti, io.BytesIO(eula_bytes))
77+
ti2 = tarfile.TarInfo("manifest.json")
78+
ti2.size = len(manifest_bytes)
79+
tf.addfile(ti2, io.BytesIO(manifest_bytes))
80+
ti3 = tarfile.TarInfo(".data_img")
81+
ti3.size = len(fat_bytes)
82+
tf.addfile(ti3, io.BytesIO(fat_bytes))
83+
84+
85+
def _make_zip_of_image(path: Path) -> None:
86+
"""Zip variant of the above (Eaton-shape in a ZIP wrapper)."""
87+
fat_bytes = _make_fat16_stub_bytes()
88+
with zipfile.ZipFile(path, "w") as zf:
89+
zf.writestr("EULA", b"LICENCE" * 32)
90+
zf.writestr("manifest.json", b'{"v":"1"}')
91+
zf.writestr("payload.img", fat_bytes)
92+
93+
94+
def _mock_upload(path: Path, filename: str):
95+
"""Build a FastAPI UploadFile-like mock that streams from a real file."""
96+
fake = MagicMock()
97+
fake.filename = filename
98+
fake.size = path.stat().st_size
99+
100+
with open(path, "rb") as f:
101+
content = f.read()
102+
103+
# read() returns progressively smaller chunks until empty
104+
offset = [0]
105+
106+
async def _read(size: int = -1):
107+
if offset[0] >= len(content):
108+
return b""
109+
if size < 0:
110+
chunk = content[offset[0]:]
111+
offset[0] = len(content)
112+
else:
113+
chunk = content[offset[0]:offset[0] + size]
114+
offset[0] += len(chunk)
115+
return chunk
116+
117+
fake.read = _read
118+
return fake
119+
120+
121+
def _make_settings(storage_root: Path):
122+
s = MagicMock()
123+
s.storage_root = str(storage_root)
124+
s.max_upload_size_mb = 2048
125+
return s
126+
127+
128+
@pytest.mark.asyncio
129+
async def test_tar_of_fat_image_does_not_shortcut(tmp_path: Path):
130+
"""Eaton-shape tar must fall through the shortcut — extracted_path None."""
131+
storage_root = tmp_path / "storage"
132+
storage_root.mkdir()
133+
tar_path = tmp_path / "vendor.tar"
134+
_make_tar_of_image(tar_path)
135+
136+
db = AsyncMock()
137+
db.add = MagicMock()
138+
db.flush = AsyncMock()
139+
140+
svc = FirmwareService(db)
141+
with patch("app.services.firmware_service.get_settings", return_value=_make_settings(storage_root)):
142+
upload = _mock_upload(tar_path, "vendor.tar")
143+
firmware = await svc.upload(
144+
project_id=uuid.UUID("00000000-0000-0000-0000-000000000001"),
145+
file=upload,
146+
)
147+
148+
# Eaton-shape: shortcut must NOT have stamped extracted_path because
149+
# find_filesystem_root correctly returned None for the FAT16 dir.
150+
assert firmware.extracted_path is None, (
151+
"tar containing a raw FS image was falsely classified as rootfs"
152+
)
153+
154+
155+
@pytest.mark.asyncio
156+
async def test_zip_of_fat_image_does_not_shortcut(tmp_path: Path):
157+
"""Parallel case — zip-rootfs shortcut must also fall through."""
158+
storage_root = tmp_path / "storage"
159+
storage_root.mkdir()
160+
zip_path = tmp_path / "vendor.zip"
161+
_make_zip_of_image(zip_path)
162+
163+
db = AsyncMock()
164+
db.add = MagicMock()
165+
db.flush = AsyncMock()
166+
167+
svc = FirmwareService(db)
168+
with patch("app.services.firmware_service.get_settings", return_value=_make_settings(storage_root)):
169+
upload = _mock_upload(zip_path, "vendor.zip")
170+
firmware = await svc.upload(
171+
project_id=uuid.UUID("00000000-0000-0000-0000-000000000001"),
172+
file=upload,
173+
)
174+
175+
assert firmware.extracted_path is None, (
176+
"zip containing a raw FS image was falsely classified as rootfs"
177+
)
178+
179+
180+
@pytest.mark.asyncio
181+
async def test_pure_rootfs_tar_still_shortcuts_with_detection_roots(tmp_path: Path):
182+
"""Control: ADB-dump shape still hits the shortcut AND gets detection_roots."""
183+
storage_root = tmp_path / "storage"
184+
storage_root.mkdir()
185+
tar_path = tmp_path / "device_dump.tar"
186+
_make_rootfs_tar(tar_path)
187+
188+
db = AsyncMock()
189+
db.add = MagicMock()
190+
db.flush = AsyncMock()
191+
192+
svc = FirmwareService(db)
193+
with patch("app.services.firmware_service.get_settings", return_value=_make_settings(storage_root)):
194+
upload = _mock_upload(tar_path, "device_dump.tar")
195+
firmware = await svc.upload(
196+
project_id=uuid.UUID("00000000-0000-0000-0000-000000000001"),
197+
file=upload,
198+
)
199+
200+
# Pure rootfs: shortcut fires, extracted_path set.
201+
assert firmware.extracted_path is not None, (
202+
"pure rootfs tar failed to classify via the shortcut"
203+
)
204+
# Rule #16: detection_roots must be populated inline.
205+
meta = firmware.device_metadata or {}
206+
roots = meta.get("detection_roots")
207+
assert isinstance(roots, list) and len(roots) >= 1, (
208+
f"detection_roots not populated on shortcut path: meta={meta!r}"
209+
)
210+
# Every root must be a string path that exists on disk.
211+
for r in roots:
212+
assert isinstance(r, str)
213+
assert os.path.isdir(r), f"detection_root does not exist: {r}"

0 commit comments

Comments
 (0)