mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
* web: decrypt PSK-1 unencrypted messages on arrival * web: address review comments * web: use proper psk to decrypt instead of alias * cover missing unit test vectors * tests: run black formatter * web: fix tests * web: refine decryption data processing logic * web: address review comments * web: cover missing unit test vectors * web: cover missing unit test vectors * web: cover missing unit test vectors * web: cover missing unit test vectors
190 lines
6.7 KiB
Ruby
190 lines
6.7 KiB
Ruby
# Copyright © 2025-26 l5yth & contributors
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
# frozen_string_literal: true
|
|
|
|
require "spec_helper"
|
|
require "fileutils"
|
|
require "tmpdir"
|
|
|
|
RSpec.describe PotatoMesh::App::Meshtastic::PayloadDecoder do
|
|
def with_env(key, value)
|
|
previous = ENV[key]
|
|
ENV[key] = value
|
|
yield
|
|
ensure
|
|
ENV[key] = previous
|
|
end
|
|
|
|
def with_repo_root(path)
|
|
allow(PotatoMesh::Config).to receive(:repo_root).and_return(path)
|
|
end
|
|
|
|
it "prefers a configured python path" do
|
|
Dir.mktmpdir do |dir|
|
|
with_env("MESHTASTIC_PYTHON", "/custom/python") do
|
|
with_repo_root(dir) do
|
|
expect(described_class.python_executable_path).to eq("/custom/python")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
it "uses the project venv when present" do
|
|
Dir.mktmpdir do |dir|
|
|
python_path = File.join(dir, "data", ".venv", "bin", "python")
|
|
FileUtils.mkdir_p(File.dirname(python_path))
|
|
File.write(python_path, "")
|
|
FileUtils.chmod(0o755, python_path)
|
|
|
|
with_env("MESHTASTIC_PYTHON", nil) do
|
|
with_repo_root(dir) do
|
|
expect(described_class.python_executable_path).to eq(python_path)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
it "falls back to python on PATH when no venv is available" do
|
|
Dir.mktmpdir do |dir|
|
|
fake_bin = File.join(dir, "bin")
|
|
FileUtils.mkdir_p(fake_bin)
|
|
python_path = File.join(fake_bin, "python3")
|
|
File.write(python_path, "#!/bin/sh\n")
|
|
FileUtils.chmod(0o755, python_path)
|
|
|
|
with_env("MESHTASTIC_PYTHON", nil) do
|
|
with_env("PATH", fake_bin) do
|
|
with_repo_root(dir) do
|
|
expect(described_class.python_executable_path).to eq(python_path)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
it "resolves the decoder script path from the repo root" do
|
|
Dir.mktmpdir do |dir|
|
|
script_path = File.join(dir, "data", "mesh_ingestor", "decode_payload.py")
|
|
FileUtils.mkdir_p(File.dirname(script_path))
|
|
File.write(script_path, "")
|
|
|
|
with_repo_root(dir) do
|
|
expect(described_class.decoder_script_path).to eq(script_path)
|
|
end
|
|
end
|
|
end
|
|
|
|
it "falls back to the web root when the repo root is unavailable" do
|
|
Dir.mktmpdir do |dir|
|
|
script_path = File.join(dir, "data", "mesh_ingestor", "decode_payload.py")
|
|
FileUtils.mkdir_p(File.dirname(script_path))
|
|
File.write(script_path, "")
|
|
|
|
with_repo_root(Dir.mktmpdir) do
|
|
allow(PotatoMesh::Config).to receive(:web_root).and_return(dir)
|
|
expect(described_class.decoder_script_path).to eq(script_path)
|
|
end
|
|
end
|
|
end
|
|
|
|
it "returns nil when the decoder script is missing" do
|
|
Dir.mktmpdir do |dir|
|
|
with_repo_root(dir) do
|
|
expect(described_class.decoder_script_path).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
it "returns nil when the decoder process fails" do
|
|
allow(described_class).to receive(:decoder_script_path).and_return("/tmp/decoder.py")
|
|
allow(described_class).to receive(:python_executable_path).and_return("/usr/bin/python3")
|
|
allow(Open3).to receive(:capture3).and_return(["{}", "boom", instance_double(Process::Status, success?: false)])
|
|
|
|
expect(described_class.decode(portnum: 3, payload_b64: "AA==")).to be_nil
|
|
end
|
|
|
|
it "returns nil when decoder output is invalid JSON" do
|
|
allow(described_class).to receive(:decoder_script_path).and_return("/tmp/decoder.py")
|
|
allow(described_class).to receive(:python_executable_path).and_return("/usr/bin/python3")
|
|
allow(Open3).to receive(:capture3).and_return(["not-json", "", instance_double(Process::Status, success?: true)])
|
|
|
|
expect(described_class.decode(portnum: 3, payload_b64: "AA==")).to be_nil
|
|
end
|
|
|
|
it "returns nil when decoder output includes an error" do
|
|
allow(described_class).to receive(:decoder_script_path).and_return("/tmp/decoder.py")
|
|
allow(described_class).to receive(:python_executable_path).and_return("/usr/bin/python3")
|
|
allow(Open3).to receive(:capture3).and_return([JSON.generate("error" => "boom"), "", instance_double(Process::Status, success?: true)])
|
|
|
|
expect(described_class.decode(portnum: 3, payload_b64: "AA==")).to be_nil
|
|
end
|
|
|
|
it "returns nil when decoder output is not a hash" do
|
|
allow(described_class).to receive(:decoder_script_path).and_return("/tmp/decoder.py")
|
|
allow(described_class).to receive(:python_executable_path).and_return("/usr/bin/python3")
|
|
allow(Open3).to receive(:capture3).and_return([JSON.generate([1, 2, 3]), "", instance_double(Process::Status, success?: true)])
|
|
|
|
expect(described_class.decode(portnum: 3, payload_b64: "AA==")).to be_nil
|
|
end
|
|
|
|
it "returns nil when the decoder executable is missing" do
|
|
allow(described_class).to receive(:decoder_script_path).and_return("/tmp/decoder.py")
|
|
allow(described_class).to receive(:python_executable_path).and_return("/missing/python")
|
|
allow(Open3).to receive(:capture3).and_raise(Errno::ENOENT)
|
|
|
|
expect(described_class.decode(portnum: 3, payload_b64: "AA==")).to be_nil
|
|
end
|
|
|
|
it "returns nil when decoder paths are unavailable" do
|
|
allow(described_class).to receive(:decoder_script_path).and_return(nil)
|
|
allow(described_class).to receive(:python_executable_path).and_return(nil)
|
|
|
|
expect(described_class.decode(portnum: 3, payload_b64: "AA==")).to be_nil
|
|
end
|
|
|
|
it "returns nil when no python executable can be found" do
|
|
with_env("MESHTASTIC_PYTHON", nil) do
|
|
with_env("PATH", "") do
|
|
with_repo_root(Dir.mktmpdir) do
|
|
expect(described_class.python_executable_path).to be_nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
it "returns nil when inputs are missing" do
|
|
expect(described_class.decode(portnum: nil, payload_b64: "AA==")).to be_nil
|
|
expect(described_class.decode(portnum: 3, payload_b64: nil)).to be_nil
|
|
end
|
|
|
|
it "falls back to PATH when configured python is blank" do
|
|
Dir.mktmpdir do |dir|
|
|
fake_bin = File.join(dir, "bin")
|
|
FileUtils.mkdir_p(fake_bin)
|
|
python_path = File.join(fake_bin, "python")
|
|
File.write(python_path, "#!/bin/sh\n")
|
|
FileUtils.chmod(0o755, python_path)
|
|
|
|
with_env("MESHTASTIC_PYTHON", " ") do
|
|
with_env("PATH", fake_bin) do
|
|
with_repo_root(dir) do
|
|
expect(described_class.python_executable_path).to eq(python_path)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|