Files
potato-mesh/web/spec/meshtastic_payload_decoder_spec.rb
l5y e0237108c6 web: decrypt PSK-1 unencrypted messages on arrival (#611)
* 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
2026-01-10 12:33:59 +01:00

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