mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-08-28 08:34:16 +00:00
test(nomadnet): add tests for query-param file downloads and docs fixes
- Add backend tests for NomadnetFileDownloader data parameter - Add WS order test verifying data reaches the downloader - Add docs_manager tests for index.html generation and directory rejection - Extend security fuzzing to cover data field in file downloads - Add frontend tests for parseNomadnetworkUrl with query strings - Add frontend tests for downloadNomadNetFile data payload - Fix meshchat.py NomadnetFileDownloader kwarg name regression
This commit is contained in:
@@ -14110,10 +14110,10 @@ class ReticulumMeshChat:
|
||||
downloader = NomadnetFileDownloader(
|
||||
destination_hash,
|
||||
file_path,
|
||||
on_file_download_success,
|
||||
on_file_download_failure,
|
||||
on_file_download_progress,
|
||||
data=request_data,
|
||||
on_file_download_success=on_file_download_success,
|
||||
on_file_download_failure=on_file_download_failure,
|
||||
on_file_download_progress=on_file_download_progress,
|
||||
on_phase=on_file_download_phase,
|
||||
reticulum=getattr(self, "reticulum", None),
|
||||
)
|
||||
|
||||
@@ -331,3 +331,38 @@ def test_extract_docs_malformed_zip(docs_manager, temp_dirs):
|
||||
finally:
|
||||
if os.path.exists(zip_path):
|
||||
os.remove(zip_path)
|
||||
|
||||
|
||||
def test_populate_meshchatx_docs_generates_index_html(tmp_path):
|
||||
public_dir = tmp_path / "public"
|
||||
public_dir.mkdir()
|
||||
docs_dir = tmp_path / "docs"
|
||||
docs_dir.mkdir()
|
||||
(docs_dir / "README.md").write_text("# Hello\nWorld")
|
||||
(docs_dir / "FAQ.md").write_text("# FAQ\nQ&A")
|
||||
|
||||
config = MagicMock()
|
||||
dm = DocsManager(config, str(public_dir), project_root=str(tmp_path))
|
||||
dm.populate_meshchatx_docs()
|
||||
|
||||
index_path = os.path.join(dm.meshchatx_docs_dir, "index.html")
|
||||
assert os.path.exists(index_path)
|
||||
content = open(index_path, encoding="utf-8").read()
|
||||
assert "MeshChatX Documentation" in content
|
||||
assert "README.html" in content
|
||||
assert "FAQ.html" in content
|
||||
|
||||
|
||||
def test_get_doc_content_rejects_directory_path(tmp_path):
|
||||
public_dir = tmp_path / "public"
|
||||
public_dir.mkdir()
|
||||
config = MagicMock()
|
||||
dm = DocsManager(config, str(public_dir))
|
||||
|
||||
# Ensure meshchatx_docs_dir exists as a directory
|
||||
os.makedirs(dm.meshchatx_docs_dir, exist_ok=True)
|
||||
|
||||
# Passing "." should resolve to the directory itself, not a file
|
||||
assert dm.get_doc_content(".") is None
|
||||
assert dm.get_doc_content("..") is None
|
||||
assert dm.get_doc_content("") is None
|
||||
|
||||
@@ -89,3 +89,53 @@ async def test_nomadnet_file_download_started_before_download_async_scheduled(
|
||||
assert events[0][1]["type"] == "nomadnet.file.download"
|
||||
assert events[0][1]["nomadnet_file_download"]["status"] == "started"
|
||||
assert events[1][0] == "run_async"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nomadnet_file_download_with_data_passed_to_downloader(
|
||||
mock_app, monkeypatch
|
||||
):
|
||||
"""Query-param data from the WS payload must reach NomadnetFileDownloader."""
|
||||
mock_app._try_serve_local_page_node_file = MagicMock(return_value=None)
|
||||
|
||||
from meshchatx.src.backend import nomadnet_downloader
|
||||
|
||||
captured = {}
|
||||
orig_init = nomadnet_downloader.NomadnetFileDownloader.__init__
|
||||
|
||||
def capturing_init(self, *args, **kwargs):
|
||||
captured["args"] = args
|
||||
captured["kwargs"] = kwargs
|
||||
# Don't call real init to avoid RNS side-effects
|
||||
self.is_cancelled = False
|
||||
self.destination_hash = args[0]
|
||||
self.path = args[1]
|
||||
self.data = kwargs.get("data")
|
||||
|
||||
monkeypatch.setattr(
|
||||
nomadnet_downloader.NomadnetFileDownloader, "__init__", capturing_init
|
||||
)
|
||||
|
||||
mock_ws = MagicMock()
|
||||
mock_ws.send_str = AsyncMock()
|
||||
|
||||
dh = "c" * 32
|
||||
await mock_app.on_websocket_data_received(
|
||||
mock_ws,
|
||||
{
|
||||
"type": "nomadnet.file.download",
|
||||
"nomadnet_file_download": {
|
||||
"destination_hash": dh,
|
||||
"file_path": "/files/report.pdf",
|
||||
"data": "version=2&format=raw",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert captured["kwargs"].get("data") == "version=2&format=raw"
|
||||
assert captured["args"][0] == bytes.fromhex(dh)
|
||||
assert captured["args"][1] == "/files/report.pdf"
|
||||
|
||||
monkeypatch.setattr(
|
||||
nomadnet_downloader.NomadnetFileDownloader, "__init__", orig_init
|
||||
)
|
||||
|
||||
@@ -188,6 +188,37 @@ def test_file_downloader_list_response_short_list_no_crash():
|
||||
on_fail.assert_called_once_with("unsupported_response")
|
||||
|
||||
|
||||
def test_file_downloader_stores_data_parameter():
|
||||
on_ok = MagicMock()
|
||||
on_fail = MagicMock()
|
||||
on_progress = MagicMock()
|
||||
fd = NomadnetFileDownloader(
|
||||
b"ab" * 8,
|
||||
"/file/data.bin",
|
||||
on_ok,
|
||||
on_fail,
|
||||
on_progress,
|
||||
data="query=value&other=123",
|
||||
)
|
||||
assert fd.data == "query=value&other=123"
|
||||
|
||||
|
||||
def test_file_downloader_passes_data_to_parent():
|
||||
on_ok = MagicMock()
|
||||
on_fail = MagicMock()
|
||||
on_progress = MagicMock()
|
||||
fd = NomadnetFileDownloader(
|
||||
b"ab" * 8,
|
||||
"/file/data.bin",
|
||||
on_ok,
|
||||
on_fail,
|
||||
on_progress,
|
||||
data="foo=bar",
|
||||
)
|
||||
# NomadnetDownloader stores data as the 3rd positional arg
|
||||
assert fd.data == "foo=bar"
|
||||
|
||||
|
||||
def test_cache_lock_serializes_mutations():
|
||||
mock_link = MagicMock()
|
||||
mock_link.status = RNS.Link.ACTIVE
|
||||
|
||||
@@ -1430,9 +1430,10 @@ def test_nomadnet_page_archive_add_fuzzing(
|
||||
@given(
|
||||
destination_hash=st.text(min_size=0, max_size=200),
|
||||
file_path=st.text(min_size=0, max_size=2000),
|
||||
data=st.one_of(st.none(), st.text(min_size=0, max_size=2000)),
|
||||
)
|
||||
def test_nomadnet_file_download_fuzzing(mock_app, destination_hash, file_path):
|
||||
"""Fuzz nomadnet.file.download WebSocket handler (path traversal, malformed hash)."""
|
||||
def test_nomadnet_file_download_fuzzing(mock_app, destination_hash, file_path, data):
|
||||
"""Fuzz nomadnet.file.download WebSocket handler (path traversal, malformed hash, data)."""
|
||||
import asyncio
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
@@ -1443,6 +1444,7 @@ def test_nomadnet_file_download_fuzzing(mock_app, destination_hash, file_path):
|
||||
"nomadnet_file_download": {
|
||||
"destination_hash": destination_hash,
|
||||
"file_path": file_path,
|
||||
"data": data,
|
||||
},
|
||||
}
|
||||
loop.run_until_complete(
|
||||
|
||||
@@ -404,4 +404,117 @@ describe("NomadNetworkPage.vue", () => {
|
||||
expect(wrapper.vm.hasPageLoadFailed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseNomadnetworkUrl", () => {
|
||||
it("parses absolute URL with query string", () => {
|
||||
const wrapper = mountNomadNetworkPage();
|
||||
const dest = "a".repeat(32);
|
||||
const result = wrapper.vm.parseNomadnetworkUrl(`${dest}:/file/report.pdf?version=2&format=raw`);
|
||||
expect(result).toEqual({
|
||||
destination_hash: dest,
|
||||
path: "/file/report.pdf",
|
||||
query: "version=2&format=raw",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses relative URL with query string", () => {
|
||||
const wrapper = mountNomadNetworkPage();
|
||||
wrapper.vm.defaultNodePagePath = "/page/index.mu";
|
||||
const result = wrapper.vm.parseNomadnetworkUrl(":/file/data.bin?key=val");
|
||||
expect(result).toEqual({
|
||||
destination_hash: null,
|
||||
path: "/file/data.bin",
|
||||
query: "key=val",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses node-only URL without query", () => {
|
||||
const wrapper = mountNomadNetworkPage();
|
||||
const dest = "b".repeat(32);
|
||||
const result = wrapper.vm.parseNomadnetworkUrl(dest);
|
||||
expect(result).toEqual({
|
||||
destination_hash: dest,
|
||||
path: wrapper.vm.defaultNodePagePath,
|
||||
query: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses absolute URL without query", () => {
|
||||
const wrapper = mountNomadNetworkPage();
|
||||
const dest = "c".repeat(32);
|
||||
const result = wrapper.vm.parseNomadnetworkUrl(`${dest}:/page/index.mu`);
|
||||
expect(result).toEqual({
|
||||
destination_hash: dest,
|
||||
path: "/page/index.mu",
|
||||
query: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null for unsupported URL", () => {
|
||||
const wrapper = mountNomadNetworkPage();
|
||||
expect(wrapper.vm.parseNomadnetworkUrl("not-a-url")).toBeNull();
|
||||
});
|
||||
|
||||
it("handles empty query string after ?", () => {
|
||||
const wrapper = mountNomadNetworkPage();
|
||||
const dest = "d".repeat(32);
|
||||
const result = wrapper.vm.parseNomadnetworkUrl(`${dest}:/file/x.txt?`);
|
||||
expect(result.path).toBe("/file/x.txt");
|
||||
expect(result.query).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("downloadNomadNetFile", () => {
|
||||
let WebSocketConnection;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Re-import to get the mocked module
|
||||
WebSocketConnection = (await import("@/js/WebSocketConnection")).default;
|
||||
WebSocketConnection.send.mockClear();
|
||||
});
|
||||
|
||||
it("includes data in websocket payload when provided", () => {
|
||||
const wrapper = mountNomadNetworkPage();
|
||||
wrapper.vm.downloadNomadNetFile(
|
||||
"a".repeat(32),
|
||||
"/file/data.bin",
|
||||
"version=2&format=raw",
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
);
|
||||
expect(WebSocketConnection.send).toHaveBeenCalledOnce();
|
||||
const payload = JSON.parse(WebSocketConnection.send.mock.calls[0][0]);
|
||||
expect(payload.type).toBe("nomadnet.file.download");
|
||||
expect(payload.nomadnet_file_download.data).toBe("version=2&format=raw");
|
||||
});
|
||||
|
||||
it("omits data field when data is null", () => {
|
||||
const wrapper = mountNomadNetworkPage();
|
||||
wrapper.vm.downloadNomadNetFile(
|
||||
"b".repeat(32),
|
||||
"/file/data.bin",
|
||||
null,
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
);
|
||||
const payload = JSON.parse(WebSocketConnection.send.mock.calls[0][0]);
|
||||
expect(payload.nomadnet_file_download).not.toHaveProperty("data");
|
||||
});
|
||||
|
||||
it("omits data field when data is undefined", () => {
|
||||
const wrapper = mountNomadNetworkPage();
|
||||
wrapper.vm.downloadNomadNetFile(
|
||||
"c".repeat(32),
|
||||
"/file/data.bin",
|
||||
undefined,
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
);
|
||||
const payload = JSON.parse(WebSocketConnection.send.mock.calls[0][0]);
|
||||
expect(payload.nomadnet_file_download).not.toHaveProperty("data");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user