blob: a1f2f9a08a248efbdcb53686942cca1ff2062c2e [file]
/*
*
* Copyright (c) 2025 Project CHIP Authors
* All rights reserved.
*
* 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.
*/
#include <pw_unit_test/framework.h>
#include <controller/CHIPDeviceController.h>
#include <controller/SetUpCodePairer.h>
#include <controller/tests/SetUpCodePairerTestAccess.h>
#include <inet/IPAddress.h>
#include <inet/InetInterface.h>
#include <lib/core/CHIPError.h>
#include <lib/dnssd/Types.h>
#include <lib/support/CodeUtils.h>
#include <system/SystemClock.h>
#include <transport/raw/PeerAddress.h>
#include <memory>
using namespace chip;
using namespace chip::Controller;
using PairerAccess = chip::Testing::SetUpCodePairerTestAccess;
namespace {
constexpr uint16_t kDiscriminatorA = 0xABC;
constexpr uint16_t kDiscriminatorB = 0xDEF;
Inet::IPAddress ParseIP(const char * str)
{
Inet::IPAddress addr;
// The callers pass valid literals; treat a parse failure as a test bug.
VerifyOrDie(Inet::IPAddress::FromString(str, addr));
return addr;
}
// InterfaceId::PlatformType is an integer on the host builds these tests run on; cast explicitly so
// the suite's -Wconversion stays clean regardless of the exact underlying type.
Inet::InterfaceId MakeInterfaceId(unsigned int id)
{
return Inet::InterfaceId(static_cast<Inet::InterfaceId::PlatformType>(id));
}
// Build a single-address commissionable-node resolution record for the given address and interface.
Dnssd::CommonResolutionData MakeResolutionData(const Inet::IPAddress & ip, Inet::InterfaceId interfaceId)
{
Dnssd::CommonResolutionData data;
data.interfaceId = interfaceId;
data.numIPs = 1;
data.ipAddress[0] = ip;
data.port = 5540;
return data;
}
// DeviceCommissioner is too large to embed in a test fixture (it exceeds
// the pw_unit_test light backend's static memory pool). Heap-allocate it
// along with the SetUpCodePairer and test accessor that depend on it.
class TestSetUpCodePairer : public ::testing::Test
{
public:
static void SetUpTestSuite() { ASSERT_EQ(Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { Platform::MemoryShutdown(); }
void SetUp() override
{
mCommissioner = std::make_unique<DeviceCommissioner>();
mPairer = std::make_unique<SetUpCodePairer>(mCommissioner.get());
mAccess = std::make_unique<PairerAccess>(mPairer.get());
}
void TearDown() override
{
mAccess.reset();
mPairer.reset();
mCommissioner.reset();
}
protected:
PairerAccess & Access() { return *mAccess; }
private:
std::unique_ptr<DeviceCommissioner> mCommissioner;
std::unique_ptr<SetUpCodePairer> mPairer;
std::unique_ptr<PairerAccess> mAccess;
};
// When the discovery timeout fires while a PASE attempt is in progress,
// DNS-SD should be stopped (it runs indefinitely) but other transports
// (BLE, Wi-Fi PAF, NFC) should be left alone since they self-terminate.
TEST_F(TestSetUpCodePairer, TimeoutDuringPASE_StopsDNSSD_PreservesOtherTransports)
{
Access().SetRemoteId(1);
Access().SetWaitingForPASE(true);
Access().SetWaitingForDiscovery(PairerAccess::kIPTransport, true);
Access().SetWaitingForDiscovery(PairerAccess::kBLETransport, true);
Access().FireTimeoutCallback();
// DNS-SD must be stopped to prevent DiscoveryInProgress() from being stuck true.
EXPECT_FALSE(Access().GetWaitingForDiscovery(PairerAccess::kIPTransport));
// BLE must be preserved — it may still discover a commissionee.
EXPECT_TRUE(Access().GetWaitingForDiscovery(PairerAccess::kBLETransport));
// PASE state must not be disturbed.
EXPECT_TRUE(Access().GetWaitingForPASE());
}
// When the discovery timeout fires with no PASE in progress,
// all transports should be stopped and failure reported.
TEST_F(TestSetUpCodePairer, TimeoutNoPASE_StopsAllTransports)
{
Access().SetRemoteId(1);
Access().SetWaitingForPASE(false);
Access().SetWaitingForDiscovery(PairerAccess::kIPTransport, true);
Access().SetWaitingForDiscovery(PairerAccess::kBLETransport, true);
Access().FireTimeoutCallback();
EXPECT_FALSE(Access().GetWaitingForDiscovery(PairerAccess::kIPTransport));
EXPECT_FALSE(Access().GetWaitingForDiscovery(PairerAccess::kBLETransport));
// StopPairingIfTransportsExhausted should have reported failure and cleared mRemoteId.
EXPECT_EQ(Access().GetRemoteId(), kUndefinedNodeId);
}
// When a PASE attempt fails (OnPairingComplete with error) and DNS-SD
// is still running, DNS-SD should NOT be stopped. Keeping DNS-SD alive
// keeps DiscoveryInProgress() true, which suppresses premature failure
// through OnStatusUpdate, TryNextRendezvousParameters, and
// StopPairingIfTransportsExhausted.
TEST_F(TestSetUpCodePairer, PASEFailure_DNSSDStaysAlive)
{
Access().SetRemoteId(1);
Access().SetWaitingForDiscovery(PairerAccess::kIPTransport, true);
Access().SetWaitingForDiscovery(PairerAccess::kBLETransport, true);
// Simulate a UDP PASE attempt: set mCurrentPASEParameters and enter
// the PASE-waiting state (as ConnectToDiscoveredDevice would).
SetUpCodePairerParameters udpParams;
udpParams.SetPeerAddress(Transport::PeerAddress::UDP(Inet::IPAddress::Any, 5540));
Access().SetCurrentPASEParameters(udpParams);
Access().ExpectPASEEstablishment();
// PASE fails.
Access().CallOnPairingComplete(CHIP_ERROR_TIMEOUT);
// DNS-SD must still be running — the 30s timeout is its natural upper bound.
EXPECT_TRUE(Access().GetWaitingForDiscovery(PairerAccess::kIPTransport));
// BLE must still be running.
EXPECT_TRUE(Access().GetWaitingForDiscovery(PairerAccess::kBLETransport));
// The pairer should still be active (not yet reported failure).
EXPECT_NE(Access().GetRemoteId(), kUndefinedNodeId);
// The error should be saved for later.
EXPECT_EQ(Access().GetLastPASEError(), CHIP_ERROR_TIMEOUT);
// mCurrentPASEParameters should have been cleared.
EXPECT_FALSE(Access().HasCurrentPASEParameters());
}
// When a PASE attempt fails synchronously in ConnectToDiscoveredDevice,
// mCurrentPASEParameters must be cleared so a later non-UDP PASE
// completion cannot incorrectly use stale parameters for ReconfirmRecord.
TEST_F(TestSetUpCodePairer, SyncPASEFailure_ClearsCurrentPASEParameters)
{
Access().SetRemoteId(1);
Access().SetWaitingForDiscovery(PairerAccess::kIPTransport, true);
// Set up stale UDP parameters (as if ConnectToDiscoveredDevice had set
// them before calling PairDevice, which then failed synchronously).
SetUpCodePairerParameters udpParams;
udpParams.SetPeerAddress(Transport::PeerAddress::UDP(Inet::IPAddress::Any, 5540));
Access().SetCurrentPASEParameters(udpParams);
// Now simulate a subsequent non-UDP PASE completion (e.g., BLE PASE
// succeeds or fails). ExpectPASEEstablishment + OnPairingComplete
// with success exercises ResetDiscoveryState which clears everything.
// Instead, verify directly that after a PASE failure the stale
// parameters don't persist: simulate a PASE completion with error.
Access().ExpectPASEEstablishment();
Access().CallOnPairingComplete(CHIP_ERROR_CONNECTION_ABORTED);
// mCurrentPASEParameters must be cleared — stale UDP params must not
// survive to confuse a later ReconfirmRecord check.
EXPECT_FALSE(Access().HasCurrentPASEParameters());
}
// A non-link-local address advertised on two different interfaces resolves to the same
// interface-less PeerAddress (we rely on the routing table, not the discovery interface), so the
// two candidates must be treated as coalescible.
TEST_F(TestSetUpCodePairer, CanCoalesce_NonLinkLocalSameAddressDifferentInterfaces)
{
const Inet::IPAddress global = ParseIP("2001:db8::1");
SetUpCodePairerParameters a(MakeResolutionData(global, MakeInterfaceId(1)), kDiscriminatorA, 0);
SetUpCodePairerParameters b(MakeResolutionData(global, MakeInterfaceId(2)), kDiscriminatorA, 0);
EXPECT_TRUE(a.CanCoalesceWith(b));
}
// A link-local address keeps its interface in the PeerAddress, so the same address on two
// interfaces is two genuinely distinct destinations and must not coalesce.
TEST_F(TestSetUpCodePairer, CannotCoalesce_LinkLocalSameAddressDifferentInterfaces)
{
const Inet::IPAddress lla = ParseIP("fe80::1");
SetUpCodePairerParameters a(MakeResolutionData(lla, MakeInterfaceId(1)), kDiscriminatorA, 0);
SetUpCodePairerParameters b(MakeResolutionData(lla, MakeInterfaceId(2)), kDiscriminatorA, 0);
EXPECT_FALSE(a.CanCoalesceWith(b));
}
// The long discriminator selects which setup payload's passcode we use, so candidates that share a
// PeerAddress but carry different discriminators must not coalesce.
TEST_F(TestSetUpCodePairer, CannotCoalesce_DifferentLongDiscriminator)
{
const Inet::IPAddress global = ParseIP("2001:db8::1");
SetUpCodePairerParameters a(MakeResolutionData(global, Inet::InterfaceId::Null()), kDiscriminatorA, 0);
SetUpCodePairerParameters b(MakeResolutionData(global, Inet::InterfaceId::Null()), kDiscriminatorB, 0);
EXPECT_FALSE(a.CanCoalesceWith(b));
}
// MRP configuration is a PASE connection input; candidates that differ only in their MRP intervals
// must not coalesce.
TEST_F(TestSetUpCodePairer, CannotCoalesce_DifferentMRPConfig)
{
const Inet::IPAddress global = ParseIP("2001:db8::1");
auto dataA = MakeResolutionData(global, Inet::InterfaceId::Null());
dataA.mrpRetryIntervalIdle = System::Clock::Milliseconds32(1000);
auto dataB = MakeResolutionData(global, Inet::InterfaceId::Null());
dataB.mrpRetryIntervalIdle = System::Clock::Milliseconds32(2000);
SetUpCodePairerParameters a(dataA, kDiscriminatorA, 0);
SetUpCodePairerParameters b(dataB, kDiscriminatorA, 0);
EXPECT_FALSE(a.CanCoalesceWith(b));
}
// Different IP addresses are different destinations and must not coalesce.
TEST_F(TestSetUpCodePairer, CannotCoalesce_DifferentAddress)
{
SetUpCodePairerParameters a(MakeResolutionData(ParseIP("2001:db8::1"), Inet::InterfaceId::Null()), kDiscriminatorA, 0);
SetUpCodePairerParameters b(MakeResolutionData(ParseIP("2001:db8::2"), Inet::InterfaceId::Null()), kDiscriminatorA, 0);
EXPECT_FALSE(a.CanCoalesceWith(b));
}
// NotifyCommissionableDeviceDiscovered must drop a duplicate address within a single advertisement
// (the same non-link-local IP listed twice) instead of queuing a redundant PASE attempt. Waiting
// for PASE is asserted so ConnectToDiscoveredDevice does not drain the queue before we inspect it.
TEST_F(TestSetUpCodePairer, NotifyDiscovered_DropsDuplicateAddressesInOneRecord)
{
Access().SetRemoteId(1);
Access().SetWaitingForPASE(true);
const Inet::IPAddress global = ParseIP("2001:db8::1");
auto data = MakeResolutionData(global, Inet::InterfaceId::Null());
data.numIPs = 2;
data.ipAddress[0] = global;
data.ipAddress[1] = global; // same address advertised twice
Access().NotifyCommissionableDeviceDiscovered(data, kDiscriminatorA);
EXPECT_EQ(Access().GetDiscoveredParametersCount(), 1u);
}
// The same non-link-local device rediscovered on a second interface must not add a second identical
// queue entry, but a genuinely different address must.
TEST_F(TestSetUpCodePairer, NotifyDiscovered_CoalescesAcrossCallbacks)
{
Access().SetRemoteId(1);
Access().SetWaitingForPASE(true);
const Inet::IPAddress global = ParseIP("2001:db8::1");
Access().NotifyCommissionableDeviceDiscovered(MakeResolutionData(global, MakeInterfaceId(1)), kDiscriminatorA);
EXPECT_EQ(Access().GetDiscoveredParametersCount(), 1u);
// Same non-link-local address, different interface -> coalesces.
Access().NotifyCommissionableDeviceDiscovered(MakeResolutionData(global, MakeInterfaceId(2)), kDiscriminatorA);
EXPECT_EQ(Access().GetDiscoveredParametersCount(), 1u);
// Different address -> new entry.
Access().NotifyCommissionableDeviceDiscovered(MakeResolutionData(ParseIP("2001:db8::2"), MakeInterfaceId(1)), kDiscriminatorA);
EXPECT_EQ(Access().GetDiscoveredParametersCount(), 2u);
}
} // namespace