| /* |
| * |
| * 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 |