// SPDX-License-Identifier: GPL-2.0-only /* * Digital Voice Modem - Test Suite * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES FROM THIS FILE HEADER. * * Copyright (C) 2026 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" #include "common/lookups/TalkgroupRulesLookup.h" #include #include #include using namespace lookups; /** * @brief Builds one talkgroup rule for admission tests. * @param tgId Talkgroup ID. * @param slot DMR slot. * @param rids Permitted radio IDs. * @returns TalkgroupRuleGroupVoice Test rule. */ static TalkgroupRuleGroupVoice admission(uint32_t tgId, uint8_t slot, std::vector rids) { TalkgroupRuleGroupVoiceSource source; source.tgId(tgId); source.tgSlot(slot); TalkgroupRuleConfig config; config.active(true); config.permittedRIDs(rids); TalkgroupRuleGroupVoice entry; entry.source(source); entry.config(config); return entry; } TEST_CASE("Talkgroup RID admission is gated by talkgroup ACL enforcement", "[network][talkgroup-admission]") { TalkgroupRulesLookup unrestricted("", 0U, false, false); unrestricted.addEntry(admission(101U, 1U, { 1001U })); REQUIRE(unrestricted.isRIDPermitted(101U, 1U, 9999U)); TalkgroupRulesLookup restricted("", 0U, true, false); restricted.addEntry(admission(101U, 1U, { 1001U, 1002U })); REQUIRE(restricted.isRIDPermitted(101U, 1U, 1001U)); REQUIRE_FALSE(restricted.isRIDPermitted(101U, 1U, 9999U)); REQUIRE_FALSE(restricted.isRIDPermitted(999U, 1U, 1001U)); } TEST_CASE("An empty permitted RID list admits every RID", "[network][talkgroup-admission]") { TalkgroupRulesLookup lookup("", 0U, true, false); lookup.addEntry(admission(202U, 0U, {})); REQUIRE(lookup.isRIDPermitted(202U, 0U, 1U)); REQUIRE(lookup.isRIDPermitted(202U, 0U, 0xFFFFFFU)); } TEST_CASE("Replacing RID admissions clears restrictions omitted by the snapshot", "[network][talkgroup-admission]") { TalkgroupRulesLookup lookup("", 0U, true, false); lookup.addEntry(admission(301U, 1U, { 3001U })); lookup.addEntry(admission(302U, 2U, { 3002U })); lookup.replaceRIDAdmissions({ admission(302U, 2U, { 4002U }) }); REQUIRE(lookup.isRIDPermitted(301U, 1U, 9999U)); REQUIRE(lookup.isRIDPermitted(302U, 2U, 4002U)); REQUIRE_FALSE(lookup.isRIDPermitted(302U, 2U, 3002U)); lookup.replaceRIDAdmissions({}); REQUIRE(lookup.isRIDPermitted(302U, 2U, 3002U)); } TEST_CASE("Talkgroup RID admission round-robin covers every TG RID and slot combination", "[network][talkgroup-admission][round-robin]") { TalkgroupRulesLookup lookup("", 0U, true, false); lookup.addEntry(admission(401U, 1U, { 1001U, 1002U })); lookup.addEntry(admission(401U, 2U, { 2001U })); lookup.addEntry(admission(402U, 0U, {})); lookup.addEntry(admission(403U, 1U, { 0U, 0xFFFFFFU })); using AdmissionCheck = std::tuple; const std::vector checks = { // Every RID is checked against both restricted slots for the same TGID. { 401U, 1U, 1001U, true }, { 401U, 1U, 1002U, true }, { 401U, 1U, 2001U, false }, { 401U, 1U, 9999U, false }, { 401U, 2U, 1001U, false }, { 401U, 2U, 1002U, false }, { 401U, 2U, 2001U, true }, { 401U, 2U, 9999U, false }, // Slot zero selects the first matching rule for protocols without slots. { 401U, 0U, 1001U, true }, { 401U, 0U, 2001U, false }, // A slot mismatch and an unknown TG are rejected while ACL is enforced. { 401U, 3U, 1001U, false }, { 499U, 0U, 1001U, false }, { 499U, 1U, 1001U, false }, // Empty lists admit all RIDs, including boundary values. { 402U, 0U, 0U, true }, { 402U, 0U, 1U, true }, { 402U, 0U, 0xFFFFFFU, true }, // Restricted lists preserve boundary RID membership behavior. { 403U, 1U, 0U, true }, { 403U, 1U, 1U, false }, { 403U, 1U, 0xFFFFFFU, true } }; // Rotate the starting point on each pass so every entry is evaluated before // and after every other admission decision, detecting leaked lookup state. for (size_t start = 0U; start < checks.size(); ++start) { for (size_t offset = 0U; offset < checks.size(); ++offset) { const AdmissionCheck& check = checks[(start + offset) % checks.size()]; const uint32_t tgId = std::get<0U>(check); const uint8_t slot = std::get<1U>(check); const uint32_t rid = std::get<2U>(check); const bool expected = std::get<3U>(check); CAPTURE(start, offset, tgId, slot, rid); REQUIRE(lookup.isRIDPermitted(tgId, slot, rid) == expected); } } } TEST_CASE("Talkgroup RID admission round-robin follows successive snapshots", "[network][talkgroup-admission][round-robin]") { TalkgroupRulesLookup lookup("", 0U, true, false); lookup.addEntry(admission(501U, 1U, {})); lookup.addEntry(admission(502U, 2U, {})); lookup.addEntry(admission(503U, 0U, {})); const std::vector tgIds = { 501U, 502U, 503U }; const std::vector slots = { 1U, 2U, 0U }; const std::vector rids = { 5001U, 5002U, 5003U }; for (size_t round = 0U; round < tgIds.size(); ++round) { lookup.replaceRIDAdmissions({ admission(tgIds[round], slots[round], { rids[round] }) }); for (size_t tg = 0U; tg < tgIds.size(); ++tg) { for (size_t rid = 0U; rid < rids.size(); ++rid) { const bool expected = tg != round || rid == round; CAPTURE(round, tg, rid, tgIds[tg], slots[tg], rids[rid]); REQUIRE(lookup.isRIDPermitted(tgIds[tg], slots[tg], rids[rid]) == expected); } } } // An unknown snapshot entry is ignored, and an empty snapshot returns all // configured TGs to unrestricted admission. lookup.replaceRIDAdmissions({ admission(599U, 0U, { 5999U }) }); for (size_t tg = 0U; tg < tgIds.size(); ++tg) { for (uint32_t rid : rids) { CAPTURE(tg, tgIds[tg], slots[tg], rid); REQUIRE(lookup.isRIDPermitted(tgIds[tg], slots[tg], rid)); } } lookup.replaceRIDAdmissions({}); for (size_t tg = 0U; tg < tgIds.size(); ++tg) { for (uint32_t rid : rids) { CAPTURE(tg, tgIds[tg], slots[tg], rid); REQUIRE(lookup.isRIDPermitted(tgIds[tg], slots[tg], rid)); } } } TEST_CASE("Disabled TG ACL admits the full round-robin matrix", "[network][talkgroup-admission][round-robin]") { TalkgroupRulesLookup lookup("", 0U, false, false); lookup.addEntry(admission(601U, 1U, { 6001U })); const std::vector tgIds = { 0U, 601U, 699U, 0xFFFFU }; const std::vector slots = { 0U, 1U, 2U, 0xFFU }; const std::vector rids = { 0U, 6001U, 6999U, 0xFFFFFFU }; for (uint32_t tgId : tgIds) { for (uint8_t slot : slots) { for (uint32_t rid : rids) { CAPTURE(tgId, slot, rid); REQUIRE(lookup.isRIDPermitted(tgId, slot, rid)); } } } }