Files
potato-mesh/web/spec/data_processing_spec.rb
l5y 9c3dae3e7d chore: refactor codebase before meshcore release (#682)
* chore: refactor codebase before meshcore release

* data: run black

* fix: resolve SonarCloud S1244/S5796 reliability issues in test files

Replace floating-point equality comparisons with pytest.approx() to
satisfy S1244, and replace the `is` identity operator with id()-based
comparison to satisfy S5796.

* fix: remove duplicate encrypted_flag assignment in store_packet_dict

The encrypted_flag was computed identically on lines 307 and 345 with no
mutation of `encrypted` between them. Remove the dead second assignment.
2026-04-04 10:22:31 +02:00

213 lines
6.6 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 "sqlite3"
RSpec.describe PotatoMesh::App::DataProcessing do
# Build a minimal host class so we can call the module methods in isolation.
let(:harness_class) do
Class.new do
include PotatoMesh::App::DataProcessing
include PotatoMesh::App::Helpers
def debug_log(message, **); end
def warn_log(message, **); end
def with_busy_retry
yield
end
def update_prometheus_metrics(*); end
def prom_report_ids
[]
end
def private_mode?
false
end
def normalize_node_id(_db, node_ref)
parts = canonical_node_parts(node_ref)
parts ? parts[0] : nil
end
def resolve_protocol(_db, _ingestor, cache: nil)
"meshtastic"
end
end
end
subject(:dp) { harness_class.new }
# ---------------------------------------------------------------------------
# coerce_bool
# ---------------------------------------------------------------------------
describe "#coerce_bool" do
it "converts true to 1" do
expect(dp.coerce_bool(true)).to eq(1)
end
it "converts false to 0" do
expect(dp.coerce_bool(false)).to eq(0)
end
it "passes through other values unchanged" do
expect(dp.coerce_bool(nil)).to be_nil
expect(dp.coerce_bool("yes")).to eq("yes")
expect(dp.coerce_bool(42)).to eq(42)
end
end
# ---------------------------------------------------------------------------
# resolve_node_num
# ---------------------------------------------------------------------------
describe "#resolve_node_num" do
it "returns an integer payload num directly" do
expect(dp.resolve_node_num("!aabbccdd", { "num" => 0xaabbccdd })).to eq(0xaabbccdd)
end
it "handles a numeric (non-integer) payload num" do
expect(dp.resolve_node_num("!aabbccdd", { "num" => 305_441_741.0 })).to eq(305_441_741)
end
it "parses a decimal string num" do
expect(dp.resolve_node_num("!aabbccdd", { "num" => "12345" })).to eq(12345)
end
it "parses a hex-prefixed string num" do
expect(dp.resolve_node_num("!aabbccdd", { "num" => "0xAABBCCDD" })).to eq(0xaabbccdd)
end
it "falls back to node_id hex when num is absent" do
expect(dp.resolve_node_num("!aabbccdd", {})).to eq(0xaabbccdd)
end
it "returns nil for invalid inputs" do
expect(dp.resolve_node_num(nil, {})).to be_nil
end
end
# ---------------------------------------------------------------------------
# canonical_node_parts
# ---------------------------------------------------------------------------
describe "#canonical_node_parts" do
it "parses !hex notation" do
parts = dp.canonical_node_parts("!aabbccdd")
expect(parts).not_to be_nil
expect(parts[0]).to eq("!aabbccdd")
expect(parts[1]).to eq(0xaabbccdd)
end
it "parses a numeric node_ref" do
parts = dp.canonical_node_parts(0xaabbccdd)
expect(parts).not_to be_nil
expect(parts[0]).to start_with("!")
expect(parts[1]).to eq(0xaabbccdd)
end
it "returns nil for an invalid string" do
expect(dp.canonical_node_parts("not_a_node")).to be_nil
end
it "returns nil for nil without a fallback" do
expect(dp.canonical_node_parts(nil)).to be_nil
end
it "uses fallback_num when node_ref is nil" do
parts = dp.canonical_node_parts(nil, 0xaabbccdd)
expect(parts).not_to be_nil
expect(parts[1]).to eq(0xaabbccdd)
end
end
# ---------------------------------------------------------------------------
# insert_telemetry (telemetry_type validation path)
# ---------------------------------------------------------------------------
describe "#insert_telemetry" do
around do |example|
Dir.mktmpdir("dp-spec-") do |dir|
db_path = File.join(dir, "mesh.db")
RSpec::Mocks.with_temporary_scope do
allow(PotatoMesh::Config).to receive(:db_path).and_return(db_path)
allow(PotatoMesh::Config).to receive(:db_busy_timeout_ms).and_return(5000)
allow(PotatoMesh::Config).to receive(:week_seconds).and_return(604_800)
allow(PotatoMesh::Config).to receive(:trace_neighbor_window_seconds).and_return(604_800)
allow(PotatoMesh::Config).to receive(:debug?).and_return(false)
db_helper = Object.new.extend(PotatoMesh::App::Database)
db_helper.init_db
db_helper.ensure_schema_upgrades
example.run
end
end
end
def open_db
db = SQLite3::Database.new(PotatoMesh::Config.db_path)
db.results_as_hash = true
db
end
let(:now) { Time.now.to_i }
let(:valid_payload) do
{
"id" => 42,
"node_id" => "!aabbccdd",
"rx_time" => now,
"telemetry_type" => "device",
"deviceMetrics" => { "batteryLevel" => 80 },
}
end
it "inserts a telemetry row with a valid telemetry_type" do
db = open_db
dp.insert_telemetry(db, valid_payload)
row = db.execute("SELECT telemetry_type FROM telemetry WHERE id = 42").first
db.close
expect(row["telemetry_type"]).to eq("device")
end
it "treats invalid telemetry_type values as nil (auto-inferred)" do
db = open_db
payload = valid_payload.merge("telemetry_type" => "bogus_type")
dp.insert_telemetry(db, payload)
row = db.execute("SELECT telemetry_type FROM telemetry WHERE id = 42").first
db.close
# "bogus_type" is not in VALID_TELEMETRY_TYPES; the value should be
# replaced by the auto-inferred type ("device") since deviceMetrics is present.
expect(%w[device nil]).to include(row["telemetry_type"].to_s)
end
it "returns nil for a non-hash payload" do
db = open_db
result = dp.insert_telemetry(db, "not a hash")
db.close
expect(result).to be_nil
end
it "returns nil when no telemetry id is present" do
db = open_db
result = dp.insert_telemetry(db, { "node_id" => "!aabbccdd" })
db.close
expect(result).to be_nil
end
end
end