| /* |
| * |
| * Copyright (c) 2026 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 "CommissioningProxyMockTimer.h" |
| #include "CommissioningProxyMockTransport.h" |
| #include <app/clusters/commissioning-proxy-server/CommissioningProxyCluster.h> |
| #include <app/clusters/commissioning-proxy-server/tests/CommissioningProxyMockTransport.h> |
| #include <platform/CommissionableDataProvider.h> // for kMaxDiscriminatorValue |
| #include <pw_unit_test/framework.h> |
| |
| #include <app/data-model-provider/tests/ReadTesting.h> |
| #include <app/server-cluster/testing/ClusterTester.h> |
| #include <app/server-cluster/testing/TestServerClusterContext.h> |
| #include <app/server-cluster/testing/ValidateGlobalAttributes.h> |
| #include <clusters/CommissioningProxy/Attributes.h> |
| #include <clusters/CommissioningProxy/Commands.h> |
| #include <clusters/CommissioningProxy/Metadata.h> |
| #include <system/SystemClock.h> |
| |
| using namespace chip; |
| using namespace chip::app; |
| using namespace chip::app::Clusters; |
| using namespace chip::app::Clusters::CommissioningProxy; |
| using namespace chip::app::Clusters::CommissioningProxy::Attributes; |
| using namespace chip::app::Clusters::CommissioningProxy::Commands; |
| using namespace chip::Testing; |
| using chip::Protocols::InteractionModel::ClusterStatusCode; |
| |
| namespace { |
| constexpr EndpointId kTestEndpointId = 1; |
| struct TestCommissioningProxyCluster : public ::testing::Test |
| { |
| // No stack init needed: every cluster timer goes through the mockTimer below, so |
| // nothing under test touches the system layer or the event loop. |
| static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } |
| static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } |
| |
| void SetUp() override {} |
| |
| // Drives the session manager's response timeout and the aggregator's scan watchdog: |
| // virtual time, so expiry costs no wall-clock, plus one-shot StartTimer failure |
| // injection for the rollback paths. |
| CommissioningProxyMockTimer mockTimer; |
| |
| // Mock transports available to every test. A transport is "supported" (advertised |
| // in the Transport attribute) iff it is registered, so RegisterMocks() makes both |
| // BLE and Wi-Fi PAF available; tests that need only one register it directly. |
| CommissioningProxyMockTransport mockBle{ CapabilitiesBitmap::kBle }; |
| CommissioningProxyMockTransport mockPaf{ CapabilitiesBitmap::kWiFiPAF }; |
| |
| void RegisterMocks(CommissioningProxyCluster & cluster) |
| { |
| cluster.RegisterTransport(mockBle); |
| cluster.RegisterTransport(mockPaf); |
| } |
| }; |
| |
| namespace CPAttributes = chip::app::Clusters::CommissioningProxy::Attributes; |
| |
| // Helper: read the Transport attribute (CapabilitiesBitmap) — the runtime |
| // source of truth for which transports the proxy supports. A transport bit |
| // is set here iff the cluster's GetSupportedTransports() returns it for the |
| // current build flag / feature flag combination. |
| static chip::BitMask<CapabilitiesBitmap> ReadSupportedTransports(ClusterTester & tester) |
| { |
| chip::BitMask<CapabilitiesBitmap> supported; |
| EXPECT_EQ(tester.ReadAttribute(CPAttributes::Transport::Id, supported), CHIP_NO_ERROR); |
| return supported; |
| } |
| |
| // Convenience: skip a test when the named transport is absent from the Transport |
| // attribute (e.g. PAF-only test running against a BLE-only build of the cluster). |
| #define SKIP_IF_TRANSPORT_UNSUPPORTED(tester, transport) \ |
| do \ |
| { \ |
| if (!ReadSupportedTransports(tester).Has(transport)) \ |
| { \ |
| GTEST_SKIP() << "Transport " #transport " not advertised by this build of CommissioningProxyCluster"; \ |
| } \ |
| } while (0) |
| |
| // Convenience: skip a test that needs more than one concurrent session when this build |
| // reserves fewer. CHIP_CONFIG_COMMISSIONING_PROXY_MAX_SESSIONS sizes the session table |
| // at compile time and the default is the spec minimum of 1, so the multi-session paths |
| // (per-fabric isolation, distinct session ids, staying Connected until the last session |
| // closes) are only reachable in a build that raises it. |
| #define SKIP_IF_MAX_SESSIONS_BELOW(needed) \ |
| do \ |
| { \ |
| if (CHIP_CONFIG_COMMISSIONING_PROXY_MAX_SESSIONS < (needed)) \ |
| { \ |
| GTEST_SKIP() << "Build reserves " << CHIP_CONFIG_COMMISSIONING_PROXY_MAX_SESSIONS \ |
| << " proxy session(s), this case needs " << (needed); \ |
| } \ |
| } while (0) |
| |
| // ============================================================================= |
| // Feature Tests |
| // ============================================================================= |
| TEST_F(TestCommissioningProxyCluster, TestFeatures) |
| { |
| TestServerClusterContext context; |
| |
| // No features - only mandatory attributes |
| { |
| BitMask<Feature> noFeatures; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(noFeatures), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| EXPECT_TRUE(IsAttributesListEqualTo(cluster, |
| { |
| CPAttributes::Transport::kMetadataEntry, |
| CPAttributes::ScanMaxTime::kMetadataEntry, |
| CPAttributes::MaxSessions::kMetadataEntry, |
| })); |
| |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> commandsBuilder; |
| EXPECT_EQ(cluster.AcceptedCommands(ConcreteClusterPath(kTestEndpointId, CommissioningProxy::Id), commandsBuilder), |
| CHIP_NO_ERROR); |
| |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> expectedCommandsBuilder; |
| EXPECT_EQ(expectedCommandsBuilder.AppendElements({ |
| ProxyConnectRequest::kMetadataEntry, |
| ProxyDisconnectRequest::kMetadataEntry, |
| ProxyScanRequest::kMetadataEntry, |
| ProxyMessageRequest::kMetadataEntry, |
| }), |
| CHIP_NO_ERROR); |
| EXPECT_TRUE(EqualAcceptedCommandSets(commandsBuilder.TakeBuffer(), expectedCommandsBuilder.TakeBuffer())); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Background Scan(BGS) Feature - BGS and mandatory attributes |
| { |
| BitMask<Feature> features(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| EXPECT_TRUE(IsAttributesListEqualTo(cluster, |
| { |
| CPAttributes::Transport::kMetadataEntry, |
| CPAttributes::ScanMaxTime::kMetadataEntry, |
| CPAttributes::MaxSessions::kMetadataEntry, |
| CPAttributes::MaxCachedResults::kMetadataEntry, |
| CPAttributes::NumCachedResults::kMetadataEntry, |
| CPAttributes::CacheTimeout::kMetadataEntry, |
| CPAttributes::CachedResults::kMetadataEntry, |
| })); |
| |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> commandsBuilder; |
| EXPECT_EQ(cluster.AcceptedCommands(ConcreteClusterPath(kTestEndpointId, CommissioningProxy::Id), commandsBuilder), |
| CHIP_NO_ERROR); |
| |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> expectedCommandsBuilder; |
| EXPECT_EQ(expectedCommandsBuilder.AppendElements({ |
| ProxyConnectRequest::kMetadataEntry, |
| ProxyDisconnectRequest::kMetadataEntry, |
| ProxyScanRequest::kMetadataEntry, |
| ProxyBackGroundScanStartRequest::kMetadataEntry, |
| ProxyBackGroundScanStopRequest::kMetadataEntry, |
| ProxyMessageRequest::kMetadataEntry, |
| }), |
| CHIP_NO_ERROR); |
| EXPECT_TRUE(EqualAcceptedCommandSets(commandsBuilder.TakeBuffer(), expectedCommandsBuilder.TakeBuffer())); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // WiFi Feature - mandatory attributes plus WiFiBand. WiFiBand is [WI] |
| // (optional under WI); this implementation always exposes it when WI is set. |
| { |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| EXPECT_TRUE(IsAttributesListEqualTo(cluster, |
| { |
| CPAttributes::Transport::kMetadataEntry, |
| CPAttributes::ScanMaxTime::kMetadataEntry, |
| CPAttributes::MaxSessions::kMetadataEntry, |
| CPAttributes::WiFiBand::kMetadataEntry, |
| })); |
| |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> commandsBuilder; |
| EXPECT_EQ(cluster.AcceptedCommands(ConcreteClusterPath(kTestEndpointId, CommissioningProxy::Id), commandsBuilder), |
| CHIP_NO_ERROR); |
| |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> expectedCommandsBuilder; |
| EXPECT_EQ(expectedCommandsBuilder.AppendElements({ |
| ProxyConnectRequest::kMetadataEntry, |
| ProxyDisconnectRequest::kMetadataEntry, |
| ProxyScanRequest::kMetadataEntry, |
| ProxyMessageRequest::kMetadataEntry, |
| }), |
| CHIP_NO_ERROR); |
| EXPECT_TRUE(EqualAcceptedCommandSets(commandsBuilder.TakeBuffer(), expectedCommandsBuilder.TakeBuffer())); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // All Features - All attributes |
| { |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| EXPECT_TRUE(IsAttributesListEqualTo(cluster, |
| { |
| CPAttributes::Transport::kMetadataEntry, |
| CPAttributes::ScanMaxTime::kMetadataEntry, |
| CPAttributes::MaxSessions::kMetadataEntry, |
| CPAttributes::MaxCachedResults::kMetadataEntry, |
| CPAttributes::NumCachedResults::kMetadataEntry, |
| CPAttributes::CacheTimeout::kMetadataEntry, |
| CPAttributes::CachedResults::kMetadataEntry, |
| CPAttributes::WiFiBand::kMetadataEntry, |
| })); |
| |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> commandsBuilder; |
| EXPECT_EQ(cluster.AcceptedCommands(ConcreteClusterPath(kTestEndpointId, CommissioningProxy::Id), commandsBuilder), |
| CHIP_NO_ERROR); |
| |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> expectedCommandsBuilder; |
| EXPECT_EQ(expectedCommandsBuilder.AppendElements({ |
| ProxyConnectRequest::kMetadataEntry, |
| ProxyDisconnectRequest::kMetadataEntry, |
| ProxyScanRequest::kMetadataEntry, |
| ProxyBackGroundScanStartRequest::kMetadataEntry, |
| ProxyBackGroundScanStopRequest::kMetadataEntry, |
| ProxyMessageRequest::kMetadataEntry, |
| }), |
| CHIP_NO_ERROR); |
| EXPECT_TRUE(EqualAcceptedCommandSets(commandsBuilder.TakeBuffer(), expectedCommandsBuilder.TakeBuffer())); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| } |
| |
| // ============================================================================= |
| // Startup Tests |
| // ============================================================================= |
| TEST_F(TestCommissioningProxyCluster, TestStartupSucceeds) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> noFeatures; |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(noFeatures), mockTimer); |
| RegisterMocks(cluster); |
| |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ============================================================================= |
| // Attribute Tests |
| // ============================================================================= |
| TEST_F(TestCommissioningProxyCluster, TestMandatoryAttributes) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> noFeatures; |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(noFeatures), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| |
| // ScanMaxTime has no spec fallback; the cluster uses 10 s as a practical default. |
| // (MaxSessions is read in TestMaxSessionsAttributeReadsFromBuildConfig, which checks it |
| // against the build constant rather than the cluster's own getter.) |
| uint8_t scanMaxTime = 0; |
| ASSERT_EQ(tester.ReadAttribute(CPAttributes::ScanMaxTime::Id, scanMaxTime), CHIP_NO_ERROR); |
| EXPECT_EQ(scanMaxTime, 10); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // The Transport attribute SHALL advertise exactly the transports that have a |
| // registered driver. kBle is not gated by any Feature bit per spec. |
| TEST_F(TestCommissioningProxyCluster, TestTransportAttribute_ReflectsRegisteredTransports) |
| { |
| TestServerClusterContext context; |
| |
| // Register only BLE: Transport advertises kBle, not kWiFiPAF. |
| { |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| cluster.RegisterTransport(mockBle); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| ClusterTester tester(cluster); |
| auto supported = ReadSupportedTransports(tester); |
| EXPECT_TRUE(supported.Has(CapabilitiesBitmap::kBle)); |
| EXPECT_FALSE(supported.Has(CapabilitiesBitmap::kWiFiPAF)); |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Register only Wi-Fi PAF: Transport advertises kWiFiPAF, not kBle. WiFiPAF is |
| // independent of the WI feature (WI only gates WiFiBand), so no feature is set. |
| { |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| cluster.RegisterTransport(mockPaf); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| ClusterTester tester(cluster); |
| auto supported = ReadSupportedTransports(tester); |
| EXPECT_TRUE(supported.Has(CapabilitiesBitmap::kWiFiPAF)); |
| EXPECT_FALSE(supported.Has(CapabilitiesBitmap::kBle)); |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Register both: Transport advertises both. |
| { |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| ClusterTester tester(cluster); |
| auto supported = ReadSupportedTransports(tester); |
| EXPECT_TRUE(supported.Has(CapabilitiesBitmap::kBle)); |
| EXPECT_TRUE(supported.Has(CapabilitiesBitmap::kWiFiPAF)); |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| } |
| |
| // MaxSessions attribute must reflect the configured GetMaxSessions() value, |
| // not a hardcoded constant. |
| TEST_F(TestCommissioningProxyCluster, TestMaxSessionsAttributeReadsFromBuildConfig) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> noFeatures; |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(noFeatures), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| |
| // MaxSessions is Fixed quality: it is the build-time pool size, not per-instance config. |
| uint8_t maxSessions = 0; |
| ASSERT_EQ(tester.ReadAttribute(CPAttributes::MaxSessions::Id, maxSessions), CHIP_NO_ERROR); |
| EXPECT_EQ(maxSessions, CHIP_CONFIG_COMMISSIONING_PROXY_MAX_SESSIONS); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ============================================================================= |
| // Spec-compliance attribute tests |
| // ============================================================================= |
| // These tests directly assert spec-mandated attribute defaults and access |
| // semantics from src/app_clusters/CommissioningProxy.adoc (§ Attributes). |
| |
| // ClusterRevision SHALL equal the highest value in the Revision History table |
| // (currently 1). Source: zzz_generated/.../Metadata.h kRevision. |
| TEST_F(TestCommissioningProxyCluster, TestClusterRevisionEqualsOne) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| uint16_t clusterRevision = 0; |
| ASSERT_EQ(tester.ReadAttribute(chip::app::Clusters::Globals::Attributes::ClusterRevision::Id, clusterRevision), CHIP_NO_ERROR); |
| EXPECT_EQ(clusterRevision, 1u); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // FeatureMap SHALL reflect the feature bits the cluster was constructed with. |
| TEST_F(TestCommissioningProxyCluster, TestFeatureMapReflectsConfig) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface, Feature::kBackgroundScan); |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| uint32_t featureMap = 0; |
| ASSERT_EQ(tester.ReadAttribute(chip::app::Clusters::Globals::Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR); |
| EXPECT_EQ(featureMap, static_cast<uint32_t>(Feature::kWiFiNetworkInterface) | static_cast<uint32_t>(Feature::kBackgroundScan)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec § Attributes: Transport, MaxSessions, MaxCachedResults and NumCachedResults are |
| // "R V" and WiFiBand is "F R V" — every one of them read-only, so a write SHALL be |
| // rejected with UnsupportedWrite. Every feature is enabled here so all five are present. |
| TEST_F(TestCommissioningProxyCluster, TestReadOnlyAttributes_WriteRejected) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface, Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| auto expectRejected = [&tester](const char * name, AttributeId id, const auto & value) { |
| auto status = tester.WriteAttribute(id, value); |
| EXPECT_FALSE(status.IsSuccess()) << name; |
| EXPECT_EQ(status.GetStatusCode().GetStatus(), Protocols::InteractionModel::Status::UnsupportedWrite) << name; |
| }; |
| |
| expectRejected("Transport", CPAttributes::Transport::Id, chip::BitMask<CapabilitiesBitmap>(CapabilitiesBitmap::kBle)); |
| expectRejected("MaxSessions", CPAttributes::MaxSessions::Id, static_cast<uint8_t>(5)); |
| expectRejected("MaxCachedResults", CPAttributes::MaxCachedResults::Id, static_cast<uint8_t>(5)); |
| expectRejected("NumCachedResults", CPAttributes::NumCachedResults::Id, static_cast<uint8_t>(3)); |
| expectRejected("WiFiBand", CPAttributes::WiFiBand::Id, chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k5g)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec § Attributes: ScanMaxTime and CacheTimeout both carry constraint "min 1", so |
| // writing 0 to either SHALL be rejected with ConstraintError. |
| TEST_F(TestCommissioningProxyCluster, TestWritableAttributes_WriteZeroConstraintError) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> bgs(Feature::kBackgroundScan); // CacheTimeout is BGS-only |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(bgs), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| |
| EXPECT_EQ(tester.WriteAttribute(CPAttributes::ScanMaxTime::Id, static_cast<uint8_t>(0)).GetStatusCode().GetStatus(), |
| Protocols::InteractionModel::Status::ConstraintError); |
| EXPECT_EQ(tester.WriteAttribute(CPAttributes::CacheTimeout::Id, static_cast<uint16_t>(0)).GetStatusCode().GetStatus(), |
| Protocols::InteractionModel::Status::ConstraintError); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec § Attributes: ScanMaxTime access = "RW VO". Storage and change reporting are owned |
| // by the cluster: a write that changes the value SHALL emit a change report and be |
| // visible to the next read, and a write of the unchanged value SHALL NOT report. |
| TEST_F(TestCommissioningProxyCluster, TestScanMaxTimeAttribute_WritableAndChangeReporting) |
| { |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| |
| // Start with the tester's context so change notifications land in the dirty |
| // list the tester observes. |
| ClusterTester tester(cluster); |
| EXPECT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR); |
| |
| // Writing the current value (default 10) is a no-op: success, no change report. |
| EXPECT_TRUE(tester.WriteAttribute(CPAttributes::ScanMaxTime::Id, static_cast<uint8_t>(10)).IsSuccess()); |
| EXPECT_FALSE(tester.IsAttributeDirty(CPAttributes::ScanMaxTime::Id)); |
| |
| // Writing a new value reports the change and round-trips through the next read. |
| EXPECT_TRUE(tester.WriteAttribute(CPAttributes::ScanMaxTime::Id, static_cast<uint8_t>(45)).IsSuccess()); |
| EXPECT_TRUE(tester.IsAttributeDirty(CPAttributes::ScanMaxTime::Id)); |
| |
| uint8_t scanMaxTime = 0; |
| ASSERT_EQ(tester.ReadAttribute(CPAttributes::ScanMaxTime::Id, scanMaxTime), CHIP_NO_ERROR); |
| EXPECT_EQ(scanMaxTime, 45u); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec § Attributes: CacheTimeout (BGS-only) fallback = 120, access = "RW VO". Storage and |
| // change reporting are owned by the cluster: a write that changes the value SHALL emit a |
| // change report and be visible to the next read, and a write of the unchanged value SHALL |
| // NOT report. |
| TEST_F(TestCommissioningProxyCluster, TestCacheTimeoutAttribute_DefaultWritableAndChangeReporting) |
| { |
| BitMask<Feature> bgs(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(bgs), mockTimer); |
| RegisterMocks(cluster); |
| |
| // Start with the tester's context so change notifications land in the dirty |
| // list the tester observes. |
| ClusterTester tester(cluster); |
| EXPECT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR); |
| |
| // Default per spec fallback column. |
| uint16_t cacheTimeout = 0; |
| ASSERT_EQ(tester.ReadAttribute(CPAttributes::CacheTimeout::Id, cacheTimeout), CHIP_NO_ERROR); |
| EXPECT_EQ(cacheTimeout, 120u); |
| |
| // Writing the current value is a no-op: success, no change report. |
| EXPECT_TRUE(tester.WriteAttribute(CPAttributes::CacheTimeout::Id, static_cast<uint16_t>(120)).IsSuccess()); |
| EXPECT_FALSE(tester.IsAttributeDirty(CPAttributes::CacheTimeout::Id)); |
| |
| // Writing a new value reports the change and round-trips through the next read. |
| EXPECT_TRUE(tester.WriteAttribute(CPAttributes::CacheTimeout::Id, static_cast<uint16_t>(60)).IsSuccess()); |
| EXPECT_TRUE(tester.IsAttributeDirty(CPAttributes::CacheTimeout::Id)); |
| ASSERT_EQ(tester.ReadAttribute(CPAttributes::CacheTimeout::Id, cacheTimeout), CHIP_NO_ERROR); |
| EXPECT_EQ(cacheTimeout, 60u); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec § Attributes: NumCachedResults (BGS-only) fallback = 0. |
| TEST_F(TestCommissioningProxyCluster, TestNumCachedResultsAttribute_DefaultZero) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> bgs(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(bgs), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| uint8_t numCachedResults = 99; // pre-set to non-zero so we know read overwrites |
| ASSERT_EQ(tester.ReadAttribute(CPAttributes::NumCachedResults::Id, numCachedResults), CHIP_NO_ERROR); |
| EXPECT_EQ(numCachedResults, 0u); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec § Attributes: MaxCachedResults (BGS-only) min 1. Reads from config. |
| TEST_F(TestCommissioningProxyCluster, TestMaxCachedResultsAttribute_ReadsFromConfig) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> bgs(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(bgs), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| uint8_t maxCachedResults = 0; |
| ASSERT_EQ(tester.ReadAttribute(CPAttributes::MaxCachedResults::Id, maxCachedResults), CHIP_NO_ERROR); |
| EXPECT_EQ(maxCachedResults, cluster.GetMaxCachedResults()); |
| EXPECT_GE(maxCachedResults, 1u); // spec: min 1 |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec § Attributes: WiFiBand (WI-only) reflects the configured supported bands. |
| TEST_F(TestCommissioningProxyCluster, TestWiFiBandAttribute_ReadsFromConfig) |
| { |
| TestServerClusterContext context; |
| chip::BitMask<WiFiBandBitmap> bands; |
| bands.Set(WiFiBandBitmap::k2g4); |
| bands.Set(WiFiBandBitmap::k5g); |
| |
| BitMask<Feature> wi(Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(wi), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(bands); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| chip::BitMask<WiFiBandBitmap> readBands; |
| ASSERT_EQ(tester.ReadAttribute(CPAttributes::WiFiBand::Id, readBands), CHIP_NO_ERROR); |
| EXPECT_TRUE(readBands.Has(WiFiBandBitmap::k2g4)); |
| EXPECT_TRUE(readBands.Has(WiFiBandBitmap::k5g)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ============================================================================= |
| // ProxyConnectRequest Command Tests |
| // ============================================================================= |
| |
| // Helper: build a minimal valid ProxyConnectRequest for a single transport. |
| static Commands::ProxyConnectRequest::Type MakeConnectRequest(CapabilitiesBitmap transport, uint16_t timeout = 30) |
| { |
| Commands::ProxyConnectRequest::Type cmd; |
| cmd.address.SetNull(); |
| cmd.transport = transport; |
| cmd.discriminator = 0; |
| cmd.vendorID = chip::VendorId::Common; |
| cmd.productID = 0; |
| cmd.timeout = timeout; |
| return cmd; |
| } |
| |
| // Establish a proxy session and return its sessionId (via the mock BLE transport). |
| static uint16_t OpenSession(ClusterTester & tester) |
| { |
| auto conn = tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kBle)); |
| EXPECT_TRUE(conn.IsSuccess()); |
| EXPECT_TRUE(conn.response.has_value()); |
| return conn.response.has_value() ? conn.response->sessionID : 0; |
| } |
| |
| // Zero transport bits SHALL be rejected. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_ZeroTransportBits) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| Commands::ProxyConnectRequest::Type cmd = MakeConnectRequest(static_cast<CapabilitiesBitmap>(0)); |
| |
| // Spec: "Exactly one transport ... SHALL be selected"; zero bits is an invalid |
| // field, and the spec mandates INVALID_COMMAND for an invalid field. |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Multiple transport bits set SHALL be rejected. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_MultipleTransportBits) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| Commands::ProxyConnectRequest::Type cmd = MakeConnectRequest(CapabilitiesBitmap::kBle); |
| cmd.transport.Set(CapabilitiesBitmap::kWiFiPAF); // now two bits set |
| |
| // Spec: "Exactly one transport ... SHALL be selected"; >1 bit is an invalid |
| // field → INVALID_COMMAND (test plan TC-2.6). |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ProxyConnectRequest: Discriminator constraint is "0 to 4095". A value |
| // above 4095 is an invalid field and SHALL return InvalidCommand. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_DiscriminatorOutOfRange) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // Exactly one transport bit set so the single-transport check passes; the |
| // discriminator check runs before the transport-supported check, so this is |
| // independent of which transports are compiled in. |
| Commands::ProxyConnectRequest::Type cmd = MakeConnectRequest(CapabilitiesBitmap::kWiFiPAF); |
| cmd.discriminator = chip::kMaxDiscriminatorValue + 1; // one past the spec max |
| |
| auto result = tester.Invoke(cmd); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A single reserved bit in transport (a bit outside the spec-defined kBle / kWiFiPAF / |
| // kNtl set) SHALL be rejected as a malformed field, not reported as a transport the |
| // proxy happens not to support: the ProxyConnectRequest Effect on Receipt reserves |
| // InvalidTransportType for a Transport "the proxy cannot support", and gives |
| // InvalidCommand for any other invalid field. This is also what the scan commands |
| // already return for a reserved bit. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_ReservedTransportBitOnly) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> wi(Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(wi), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // 0x01 is a reserved bit (kBle=0x02, kWiFiPAF=0x08, kNtl=0x10). |
| Commands::ProxyConnectRequest::Type cmd = MakeConnectRequest(static_cast<CapabilitiesBitmap>(0x01)); |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // The WiFiPAF CapabilitiesBitmap conformance is O.a+, independent of the WI feature: WI |
| // gates only the WiFiBand field, which is absent here. So the same request SHALL be |
| // accepted whether or not WI is enabled, and SHALL return a sessionId. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_WiFiPAFIndependentOfWIFeature) |
| { |
| TestServerClusterContext context; |
| |
| for (const BitMask<Feature> features : { BitMask<Feature>{}, BitMask<Feature>(Feature::kWiFiNetworkInterface) }) |
| { |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| |
| auto result = tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kWiFiPAF)); |
| EXPECT_TRUE(result.IsSuccess()); |
| ASSERT_TRUE(result.response.has_value()); |
| if (result.response.has_value()) |
| { |
| EXPECT_EQ(result.response->sessionID, 1u); |
| } |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| } |
| |
| // WiFiBand present with a non-WiFiPAF transport SHALL return InvalidCommand |
| // (the WiFiBand field is only meaningful for the WiFiPAF transport per spec). |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_WiFiBandWithBleTransport) |
| { |
| TestServerClusterContext context; |
| // Enable WI so the cluster has wiFiBand plumbing wired; the rejection here |
| // is driven by the transport != kWiFiPAF check, not the WI feature bit. |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| Commands::ProxyConnectRequest::Type cmd = MakeConnectRequest(CapabilitiesBitmap::kBle); |
| cmd.wiFiBand.SetValue(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // WiFiBand field present with WI feature enabled and band in supported set — SHALL succeed. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_WiFiBandWithWIFeature) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| Commands::ProxyConnectRequest::Type cmd = MakeConnectRequest(CapabilitiesBitmap::kWiFiPAF); |
| cmd.wiFiBand.SetValue(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| |
| EXPECT_TRUE(tester.Invoke(cmd).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Reserved bits in wiFiBand SHALL be rejected. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_ReservedWiFiBandBits) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| Commands::ProxyConnectRequest::Type cmd = MakeConnectRequest(CapabilitiesBitmap::kWiFiPAF); |
| // bit 1 (0x02) is reserved. |
| cmd.wiFiBand.SetValue(chip::BitMask<WiFiBandBitmap>(static_cast<WiFiBandBitmap>(0x02))); |
| |
| // Impl-defined: the spec mandates INVALID_TRANSPORT_TYPE only for an *unsupported* |
| // WiFiBand; it does not mandate a specific status for reserved WiFiBand bits. This |
| // asserts current behavior (INVALID_COMMAND for a malformed field). |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // wiFiBand not in the proxy's supported bands SHALL return InvalidTransportType. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_WiFiBandNotInSupportedBands) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| // Proxy only supports 2.4 GHz. |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| Commands::ProxyConnectRequest::Type cmd = MakeConnectRequest(CapabilitiesBitmap::kWiFiPAF); |
| // Request 5 GHz — not in supported bands. |
| cmd.wiFiBand.SetValue(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k5g)); |
| |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidTransportType)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Whenever the proxy advertises kBle, ProxyConnectRequest with kBle SHALL succeed and |
| // return a SessionID, and the cluster state SHALL move to kState_CPConnected. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_StateTransitionOnSuccess) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| EXPECT_EQ(cluster.GetCPState(), CommissioningProxyCluster::kState_CPDisconnected); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| auto result = tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kBle)); |
| EXPECT_TRUE(result.IsSuccess()); |
| ASSERT_TRUE(result.response.has_value()); |
| if (result.response.has_value()) |
| { |
| EXPECT_EQ(result.response->sessionID, 1u); |
| } |
| |
| EXPECT_EQ(cluster.GetCPState(), CommissioningProxyCluster::kState_CPConnected); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A failed ProxyConnectRequest SHALL NOT change the cluster state. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_StateUnchangedOnFailure) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // A reserved transport bit (0x01) is always rejected, whatever transports the build |
| // registers, so this is a build-independent connect failure that must not transition |
| // state. |
| EXPECT_FALSE(tester.Invoke(MakeConnectRequest(static_cast<CapabilitiesBitmap>(0x01))).IsSuccess()); |
| |
| EXPECT_EQ(cluster.GetCPState(), CommissioningProxyCluster::kState_CPDisconnected); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Per the ProxyConnectRequest Effect on Receipt: if the number of active sessions |
| // has reached the value of the MaxSessions attribute, a RESOURCE_EXHAUSTED status |
| // SHALL be returned. Enforced generically by the cluster from |
| // GetActiveSessionCount() vs the configured MaxSessions, so every transport inherits the |
| // behaviour without having to remember the check itself. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_ResourceExhaustedAtMaxSessions) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| |
| // Fill the session pool, then check the next connect is refused. A pending connect |
| // also counts toward the active-session total, so the last slot is taken that way. |
| for (uint8_t i = 0; i < CHIP_CONFIG_COMMISSIONING_PROXY_MAX_SESSIONS - 1; i++) |
| { |
| EXPECT_TRUE(tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kBle)).IsSuccess()); |
| } |
| mockBle.SetConnectPending(true); |
| |
| auto result = tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kBle)); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::ResourceExhausted)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Below MaxSessions: the cluster-level pre-check does not reject; the |
| // request is forwarded to the transport driver and (for the mock) succeeds. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_BelowMaxSessionsSucceeds) |
| { |
| SKIP_IF_MAX_SESSIONS_BELOW(2); |
| |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| // First connect establishes one session, leaving at least one slot free. |
| EXPECT_TRUE(tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kBle)).IsSuccess()); |
| |
| // Second connect is still below MaxSessions, so the gate passes and it succeeds. |
| // (The MaxSessions == 1 exhaustion case is covered by _ResourceExhaustedAtMaxSessions.) |
| EXPECT_TRUE(tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kBle)).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Per the ProxyConnectRequest Effect on Receipt: if Timeout expires, the connection |
| // attempt is terminated and a TIMEOUT status SHALL be returned. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_TransportTimeout_Propagated) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| // The transport's connect times out; the cluster SHALL surface TIMEOUT. |
| mockBle.SetConnectStatus(Protocols::InteractionModel::Status::Timeout); |
| |
| ClusterTester tester(cluster); |
| auto result = tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kBle)); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::Timeout)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ============================================================================= |
| // ProxyDisconnectRequest Command Tests |
| // ============================================================================= |
| |
| // Disconnecting with the sessionId the connect returned SHALL succeed, and with no |
| // session left the cluster state SHALL revert to kState_CPDisconnected. |
| TEST_F(TestCommissioningProxyCluster, TestProxyDisconnectRequest_StateTransitionToDisconnected) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| uint16_t sid = OpenSession(tester); |
| EXPECT_EQ(cluster.GetCPState(), CommissioningProxyCluster::kState_CPConnected); |
| |
| Commands::ProxyDisconnectRequest::Type cmd; |
| cmd.sessionID.SetNonNull(sid); |
| EXPECT_TRUE(tester.Invoke(cmd).IsSuccess()); |
| |
| // State SHALL be Disconnected after a successful disconnect. |
| EXPECT_EQ(cluster.GetCPState(), CommissioningProxyCluster::kState_CPDisconnected); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // With MaxSessions > 1, disconnecting one of several active sessions SHALL NOT |
| // transition the cluster to disconnected; only the final disconnect (no sessions |
| // remaining) SHALL do so. |
| TEST_F(TestCommissioningProxyCluster, TestProxyDisconnectRequest_MultiSessionStateTransition) |
| { |
| SKIP_IF_MAX_SESSIONS_BELOW(2); |
| |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| |
| // Open two sessions and capture their (distinct) session IDs. |
| auto r1 = tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kBle)); |
| ASSERT_TRUE(r1.IsSuccess()); |
| ASSERT_TRUE(r1.response.has_value()); |
| auto r2 = tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kBle)); |
| ASSERT_TRUE(r2.IsSuccess()); |
| ASSERT_TRUE(r2.response.has_value()); |
| EXPECT_EQ(cluster.GetCPState(), CommissioningProxyCluster::kState_CPConnected); |
| |
| Commands::ProxyDisconnectRequest::Type cmd; |
| |
| // First disconnect: one session remains, so state SHALL stay Connected. |
| if (r1.response.has_value()) |
| { |
| cmd.sessionID.SetNonNull(r1.response->sessionID); |
| } |
| EXPECT_TRUE(tester.Invoke(cmd).IsSuccess()); |
| EXPECT_EQ(cluster.GetCPState(), CommissioningProxyCluster::kState_CPConnected); |
| |
| // Second disconnect: no sessions remain, so state SHALL revert to Disconnected. |
| if (r2.response.has_value()) |
| { |
| cmd.sessionID.SetNonNull(r2.response->sessionID); |
| } |
| EXPECT_TRUE(tester.Invoke(cmd).IsSuccess()); |
| EXPECT_EQ(cluster.GetCPState(), CommissioningProxyCluster::kState_CPDisconnected); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // When the transport fails to disconnect, the command SHALL fail and the cluster |
| // state SHALL remain Connected (session not cleaned up). |
| TEST_F(TestCommissioningProxyCluster, TestProxyDisconnectRequest_TransportFailurePreservesState) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| |
| auto connectResult = tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kBle)); |
| ASSERT_TRUE(connectResult.IsSuccess()); |
| ASSERT_TRUE(connectResult.response.has_value()); |
| EXPECT_EQ(cluster.GetCPState(), CommissioningProxyCluster::kState_CPConnected); |
| |
| // The transport rejects the disconnect. |
| mockBle.SetDisconnectStatus(Protocols::InteractionModel::Status::Failure); |
| |
| Commands::ProxyDisconnectRequest::Type cmd; |
| if (connectResult.response.has_value()) |
| { |
| cmd.sessionID.SetNonNull(connectResult.response->sessionID); |
| } |
| EXPECT_FALSE(tester.Invoke(cmd).IsSuccess()); |
| |
| // State SHALL remain Connected since the transport rejected the disconnect. |
| EXPECT_EQ(cluster.GetCPState(), CommissioningProxyCluster::kState_CPConnected); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A null SessionID with a pending connect SHALL return Success and cancel the connect. |
| TEST_F(TestCommissioningProxyCluster, TestProxyDisconnectRequest_CancelPending_Success) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| |
| // A connect is pending on the BLE transport, owned by fabric 2. |
| mockBle.SetPendingConnectFabric(2); |
| |
| tester.SetFabricIndex(2); |
| Commands::ProxyDisconnectRequest::Type cmd; |
| cmd.sessionID.SetNull(); |
| EXPECT_TRUE(tester.Invoke(cmd).IsSuccess()); |
| EXPECT_TRUE(mockBle.CancelCalled()); |
| EXPECT_EQ(mockBle.LastCancelFabric(), 2); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A null SessionID with no ProxyConnectRequest in flight (e.g. already |
| // connected, or never started) has no pending connect to cancel. |
| TEST_F(TestCommissioningProxyCluster, TestProxyDisconnectRequest_CancelPending_AlreadyConnected) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| |
| // CancelPendingConnect inherits InvalidInState from the transport driver (no pending connect). |
| Commands::ProxyDisconnectRequest::Type cmd; |
| cmd.sessionID.SetNull(); |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidInState)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A null SessionID from the wrong fabric SHALL return NotFound (fabric isolation). |
| TEST_F(TestCommissioningProxyCluster, TestProxyDisconnectRequest_CancelPending_WrongFabric) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| |
| // A connect is pending on the BLE transport, owned by fabric 1. |
| mockBle.SetPendingConnectFabric(1); |
| |
| Commands::ProxyDisconnectRequest::Type cmd; |
| cmd.sessionID.SetNull(); |
| |
| // Fabric 2 tries to cancel fabric 1's pending connect — SHALL be rejected. |
| tester.SetFabricIndex(2); |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::NotFound)); |
| |
| // Fabric 1 (the owner) cancels its own pending connect — SHALL succeed. |
| tester.SetFabricIndex(1); |
| EXPECT_TRUE(tester.Invoke(cmd).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Per the ProxyDisconnectRequest Effect on Receipt: if no transport connection |
| // with the specified SessionId exists (or the connection's fabric does not match |
| // the sending fabric), the command SHALL be rejected with NOT_FOUND. The cluster |
| // returns NotFound; cluster must propagate. |
| TEST_F(TestCommissioningProxyCluster, TestProxyDisconnectRequest_UnknownSession_NotFound) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| Commands::ProxyDisconnectRequest::Type cmd; |
| cmd.sessionID.SetNonNull(1234); // no such session registered |
| |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::NotFound)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ============================================================================= |
| // ProxyScanRequest Command Tests |
| // ============================================================================= |
| |
| // Zero transport bits SHALL be rejected. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_ZeroTransport) |
| { |
| TestServerClusterContext context; |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| Commands::ProxyScanRequest::Type command; |
| command.transport = static_cast<CapabilitiesBitmap>(0); |
| |
| // Impl-defined: the ProxyScanRequest spec defines only a BUSY status; it mandates |
| // no specific status for a zero/malformed transport. This asserts current behavior. |
| auto result = tester.Invoke(Commands::ProxyScanRequest::Id, command); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Reserved bits in transport SHALL be rejected. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_ReservedTransportBits) |
| { |
| TestServerClusterContext context; |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // kWiFiPAF(0x08) | reserved(0x01) = 0x09 contains a reserved bit. |
| Commands::ProxyScanRequest::Type command; |
| command.transport = static_cast<CapabilitiesBitmap>(0x09); |
| |
| // Impl-defined: the ProxyScanRequest spec defines only a BUSY status; it mandates |
| // no specific status for reserved transport bits. This asserts current behavior. |
| auto result = tester.Invoke(Commands::ProxyScanRequest::Id, command); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Whenever the proxy advertises kBle in the Transport attribute, |
| // ProxyScanRequest with kBle |
| // SHALL succeed regardless of which Feature bits the cluster was constructed |
| // with. Skips when the build does not include BLE. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_BleNoFeaturesSucceeds) |
| { |
| TestServerClusterContext context; |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| Commands::ProxyScanRequest::Type command; |
| command.transport = CapabilitiesBitmap::kBle; |
| |
| EXPECT_TRUE(tester.Invoke(Commands::ProxyScanRequest::Id, command).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ProxyScanRequest with both kBle and kWiFiPAF in a single transport mask |
| // (with the WI feature enabled) SHALL succeed. Skips when either transport |
| // is missing from the build. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_BleAndWiFiPAFTogether) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| |
| Commands::ProxyScanRequest::Type command; |
| command.transport.Set(CapabilitiesBitmap::kBle); |
| command.transport.Set(CapabilitiesBitmap::kWiFiPAF); |
| |
| // Each mock transport contributes 2 results; the aggregator SHALL combine both |
| // sub-scans into a single ProxyScanResponse and report the total count. |
| auto result = tester.Invoke(command); |
| ASSERT_TRUE(result.IsSuccess()); |
| ASSERT_TRUE(result.response.has_value()); |
| if (result.response.has_value()) |
| { |
| EXPECT_EQ(result.response->numberOfResults, 4u); |
| size_t listCount = 0; |
| auto iter = result.response->proxyScanResult.begin(); |
| while (iter.Next()) |
| { |
| ++listCount; |
| } |
| EXPECT_EQ(iter.GetStatus(), CHIP_NO_ERROR); |
| EXPECT_EQ(listCount, 4u); |
| } |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // The cluster registers each sub-scan only after starting it, so a transport reporting |
| // synchronously from inside its own Scan() satisfies the expected count while later |
| // transports have yet to be counted. Emitting there would answer the commissioner with a |
| // ProxyScanResponse missing every transport the cluster had not reached yet. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_SyncContributorWaitsForRemainingSubScans) |
| { |
| TestServerClusterContext context; |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| |
| // BLE is scanned first and reports later, as a real driver does from its own scan |
| // timer; PAF then reports synchronously from inside Scan(). |
| mockBle.SetAutoContribute(false); |
| mockPaf.SetAutoContribute(true); |
| |
| Commands::ProxyScanRequest::Type command; |
| command.transport.Set(CapabilitiesBitmap::kBle); |
| command.transport.Set(CapabilitiesBitmap::kWiFiPAF); |
| [[maybe_unused]] auto pending = tester.Invoke(command); |
| |
| // PAF's synchronous report must not have closed the aggregation on its own. |
| EXPECT_FALSE(tester.GetCommandHandler().HasResponse()); |
| |
| // BLE reports; with every sub-scan in, the response carries both transports' results. |
| mockBle.ContributeScanResults(); |
| ASSERT_TRUE(tester.GetCommandHandler().HasResponse()); |
| Commands::ProxyScanResponse::DecodableType response; |
| ASSERT_EQ(tester.GetCommandHandler().DecodeResponse(response), CHIP_NO_ERROR); |
| EXPECT_EQ(response.numberOfResults, 4u); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // The kWiFiPAF transport bit is O.a+, independent of WI: WI gates only the wiFiBands |
| // field, which is absent here. So the same scan SHALL be accepted either way. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_WiFiPAFIndependentOfWIFeature) |
| { |
| TestServerClusterContext context; |
| |
| for (const BitMask<Feature> features : { BitMask<Feature>{}, BitMask<Feature>(Feature::kWiFiNetworkInterface) }) |
| { |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| |
| Commands::ProxyScanRequest::Type command; |
| command.transport = CapabilitiesBitmap::kWiFiPAF; |
| // No wiFiBands — skips the band validation entirely. |
| |
| EXPECT_TRUE(tester.Invoke(Commands::ProxyScanRequest::Id, command).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| } |
| |
| // WiFiBands field present without the WI feature is rejected. The kWiFiPAF transport |
| // itself is supported without WI; the rejection is driven solely by the |
| // wiFiBands-requires-WI guard. Impl-defined status: the spec does not mandate a |
| // specific status for a WI-conformance field sent without WI; this asserts current |
| // behavior (INVALID_COMMAND). |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_WiFiBandWithoutWIFeature) |
| { |
| TestServerClusterContext context; |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| |
| Commands::ProxyScanRequest::Type command; |
| command.transport = CapabilitiesBitmap::kWiFiPAF; |
| command.wiFiBands.SetValue(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| |
| auto result = tester.Invoke(Commands::ProxyScanRequest::Id, command); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Reserved bits in wiFiBands SHALL be rejected. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_ReservedWiFiBandBits) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // k2g4=0x01, k5g=0x04 are valid; bit 1 (0x02) is reserved. |
| Commands::ProxyScanRequest::Type command; |
| command.transport = CapabilitiesBitmap::kWiFiPAF; |
| command.wiFiBands.SetValue(chip::BitMask<WiFiBandBitmap>(static_cast<WiFiBandBitmap>(0x02))); |
| |
| // Impl-defined: the spec mandates INVALID_TRANSPORT_TYPE only for unsupported |
| // bands, not for reserved WiFiBand bits. This asserts current behavior. |
| auto result = tester.Invoke(Commands::ProxyScanRequest::Id, command); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // wiFiBands containing bits not in the proxy's supported bands SHALL return |
| // InvalidTransportType. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_WiFiBandNotInSupportedBands) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| // Proxy only supports 2.4 GHz. |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // Request 5 GHz — not in supported bands. |
| Commands::ProxyScanRequest::Type command; |
| command.transport = CapabilitiesBitmap::kWiFiPAF; |
| command.wiFiBands.SetValue(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k5g)); |
| |
| auto result = tester.Invoke(Commands::ProxyScanRequest::Id, command); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidTransportType)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // kWiFiPAF with WI feature and a wiFiBands value within supported bands — SHALL succeed. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_ValidWiFiBandInSupportedBands) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| // Proxy supports both 2.4 GHz and 5 GHz. |
| chip::BitMask<WiFiBandBitmap> bothBands; |
| bothBands.Set(WiFiBandBitmap::k2g4); |
| bothBands.Set(WiFiBandBitmap::k5g); |
| |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(bothBands); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| Commands::ProxyScanRequest::Type command; |
| command.transport = CapabilitiesBitmap::kWiFiPAF; |
| command.wiFiBands.SetValue(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| |
| EXPECT_TRUE(tester.Invoke(Commands::ProxyScanRequest::Id, command).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ============================================================================= |
| // ProxyMessageRequest Command Tests |
| // ============================================================================= |
| |
| // ProxyMessageRequest with a non-null message SHALL succeed and return a |
| // ProxyMessageResponse carrying the same sessionId. |
| TEST_F(TestCommissioningProxyCluster, TestProxyMessageRequest_WithMessage) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| uint16_t sid = OpenSession(tester); |
| |
| static const uint8_t kMsg[] = { 0x01, 0x02, 0x03, 0x04 }; |
| Commands::ProxyMessageRequest::Type cmd; |
| cmd.sessionID = sid; |
| cmd.responseTimeout = 5; |
| cmd.message.SetNonNull(chip::ByteSpan(kMsg, sizeof(kMsg))); |
| |
| // The mock transport delivers an immediate (null) commissionee reply. |
| auto result = tester.Invoke(cmd); |
| EXPECT_TRUE(result.IsSuccess()); |
| ASSERT_TRUE(result.response.has_value()); |
| if (result.response.has_value()) |
| { |
| EXPECT_EQ(result.response->sessionID, sid); |
| } |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A null ProxyMessageRequest.message SHALL succeed and |
| // return a ProxyMessageResponse with a null message (no data from commissionee). |
| TEST_F(TestCommissioningProxyCluster, TestProxyMessageRequest_NullMessage_Poll) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| uint16_t sid = OpenSession(tester); |
| |
| Commands::ProxyMessageRequest::Type cmd; |
| cmd.sessionID = sid; |
| cmd.responseTimeout = 5; |
| cmd.message.SetNull(); |
| |
| auto result = tester.Invoke(cmd); |
| EXPECT_TRUE(result.IsSuccess()); |
| ASSERT_TRUE(result.response.has_value()); |
| if (result.response.has_value()) |
| { |
| EXPECT_EQ(result.response->sessionID, sid); |
| // Null response message signals no pending data from commissionee. |
| EXPECT_TRUE(result.response->message.IsNull()); |
| } |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Per the ProxyMessageRequest Effect on Receipt: if no transport connection with |
| // the specified SessionID exists (or the fabric does not match), the command SHALL |
| // be rejected with NOT_FOUND. The cluster's session table enforces this. |
| TEST_F(TestCommissioningProxyCluster, TestProxyMessageRequest_UnknownSession_NotFound) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| |
| static const uint8_t kMsg[] = { 0xFF }; |
| Commands::ProxyMessageRequest::Type cmd; |
| cmd.sessionID = 9999; // no such session |
| cmd.responseTimeout = 5; |
| cmd.message.SetNonNull(chip::ByteSpan(kMsg, sizeof(kMsg))); |
| |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::NotFound)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Per the ProxyMessageRequest Effect on Receipt: if another ProxyMessageRequest |
| // referencing the same SessionId is still outstanding, the command SHALL be |
| // rejected with BUSY. The cluster's session manager enforces this. |
| TEST_F(TestCommissioningProxyCluster, TestProxyMessageRequest_DuplicateRequest_Busy) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| uint16_t sid = OpenSession(tester); |
| |
| // Leave the first request pending (no commissionee reply), so the second one |
| // for the same session hits the BUSY path. |
| mockBle.SetAutoRespond(false); |
| |
| static const uint8_t kMsg[] = { 0xFF }; |
| Commands::ProxyMessageRequest::Type cmd; |
| cmd.sessionID = sid; |
| cmd.responseTimeout = 5; |
| cmd.message.SetNonNull(chip::ByteSpan(kMsg, sizeof(kMsg))); |
| |
| [[maybe_unused]] auto pending = tester.Invoke(cmd); // first request: stays pending |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::Busy)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ============================================================================= |
| // ProxyBackgroundScanStartRequest — parameter validation tests |
| // ============================================================================= |
| |
| // Helper: build a minimal valid ProxyBackgroundScanStartRequest. |
| static Commands::ProxyBackGroundScanStartRequest::Type |
| MakeBgScanStartRequest(CapabilitiesBitmap transport, uint16_t timeout = 30, |
| chip::Optional<chip::BitMask<WiFiBandBitmap>> wiFiBands = chip::NullOptional) |
| { |
| Commands::ProxyBackGroundScanStartRequest::Type cmd; |
| cmd.transport = transport; |
| cmd.timeout = timeout; |
| cmd.wiFiBands = wiFiBands; |
| return cmd; |
| } |
| |
| // transport=0 SHALL be rejected. |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStart_ZeroTransport) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // Impl-defined: the spec mandates INVALID_TRANSPORT_TYPE only for an unsupported |
| // transport/band on BgScanStart; it does not mandate a status for a zero transport. |
| // This asserts current behavior. |
| auto result = tester.Invoke(MakeBgScanStartRequest(static_cast<CapabilitiesBitmap>(0))); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Reserved bits in transport SHALL be rejected. |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStart_ReservedTransportBits) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // CapabilitiesBitmap: kBle=0x02, kWiFiPAF=0x08; all other bits are reserved. |
| // 0x09 = kWiFiPAF(0x08) | reserved(0x01) → contains a reserved bit. |
| // Impl-defined: the spec mandates no specific status for reserved transport bits |
| // on BgScanStart. This asserts current behavior. |
| auto result = tester.Invoke(MakeBgScanStartRequest(static_cast<CapabilitiesBitmap>(0x09))); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // kBle is not gated by any Feature bit in the spec; whenever the proxy |
| // advertises kBle in the Transport attribute, ProxyBackGroundScanStartRequest |
| // with kBle SHALL succeed (only the BGS feature is required for the command |
| // to be accepted at all). |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStart_BleNoExtraFeaturesSucceeds) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| EXPECT_TRUE(tester.Invoke(MakeBgScanStartRequest(CapabilitiesBitmap::kBle)).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // kWiFiPAF without WI feature SHALL be accepted: the WiFiPAF transport bit is |
| // O.a+, independent of WI. WI only gates the wiFiBands field (absent here). |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStart_WiFiPAFWithoutWIFeature) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan); // no kWiFiNetworkInterface |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| |
| EXPECT_TRUE(tester.Invoke(MakeBgScanStartRequest(CapabilitiesBitmap::kWiFiPAF)).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // wiFiBands with reserved bits SHALL be rejected. |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStart_ReservedWiFiBandBits) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // k2g4=0x01, k5g=0x04 are valid; bit 1 (0x02) is reserved. |
| auto cmd = MakeBgScanStartRequest(CapabilitiesBitmap::kWiFiPAF, 30, |
| chip::MakeOptional(chip::BitMask<WiFiBandBitmap>(static_cast<WiFiBandBitmap>(0x02)))); |
| // Impl-defined: the spec mandates INVALID_TRANSPORT_TYPE only for unsupported |
| // bands, not reserved WiFiBand bits. This asserts current behavior. |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // kWiFiPAF with a wiFiBands field but no WI feature is rejected. The kWiFiPAF |
| // transport itself is supported without WI; the rejection is driven solely by the |
| // wiFiBands-requires-WI guard. Impl-defined status: the spec does not mandate a |
| // specific status for a WI-conformance field sent without WI; this asserts current |
| // behavior (INVALID_COMMAND). |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStart_WiFiPAFAndBandWithoutWIFeature) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan); // no kWiFiNetworkInterface |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| |
| auto cmd = MakeBgScanStartRequest(CapabilitiesBitmap::kWiFiPAF, 30, |
| chip::MakeOptional(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4))); |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Valid kWiFiPAF with wiFiBands inside the supported set — SHALL succeed, for a single |
| // band and for a multi-bit mask ("Multiple frequency bands can be selected for the scan"). |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStart_ValidWiFiPAFBands) |
| { |
| TestServerClusterContext context; |
| chip::BitMask<WiFiBandBitmap> bothBands; |
| bothBands.Set(WiFiBandBitmap::k2g4); |
| bothBands.Set(WiFiBandBitmap::k5g); |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(bothBands); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| |
| EXPECT_TRUE(tester |
| .Invoke(MakeBgScanStartRequest(CapabilitiesBitmap::kWiFiPAF, 30, |
| chip::MakeOptional(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)))) |
| .IsSuccess()); |
| EXPECT_TRUE(tester.Invoke(MakeBgScanStartRequest(CapabilitiesBitmap::kWiFiPAF, 30, chip::MakeOptional(bothBands))).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // kWiFiPAF with a WiFiBand bit not in GetSupportedWiFiBands() SHALL return INVALID_TRANSPORT_TYPE. |
| // Proxy only supports 2.4 GHz; requesting 5 GHz must be rejected. |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStart_UnsupportedWiFiBand) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| auto cmd = MakeBgScanStartRequest(CapabilitiesBitmap::kWiFiPAF, 30, |
| chip::MakeOptional(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k5g))); |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidTransportType)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ============================================================================= |
| // ProxyBackgroundScanStopRequest — parameter validation tests |
| // ============================================================================= |
| |
| // Helper: build a ProxyBackgroundScanStopRequest. |
| static Commands::ProxyBackGroundScanStopRequest::Type |
| MakeBgScanStopRequest(CapabilitiesBitmap transport, chip::Optional<chip::BitMask<WiFiBandBitmap>> wiFiBands = chip::NullOptional) |
| { |
| Commands::ProxyBackGroundScanStopRequest::Type cmd; |
| cmd.transport = transport; |
| cmd.wiFiBands = wiFiBands; |
| return cmd; |
| } |
| |
| // transport=0 and no wiFiBands SHALL be rejected (nothing to stop). |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStop_ZeroTransportNoWiFiBands) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // Impl-defined: the BgScanStop spec defines only NOT_FOUND and SUCCESS; it mandates |
| // no specific status for an empty (transport=0, no bands) request. This asserts |
| // current behavior. |
| auto result = tester.Invoke(MakeBgScanStopRequest(static_cast<CapabilitiesBitmap>(0))); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Reserved bits in transport SHALL be rejected. |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStop_ReservedTransportBits) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // CapabilitiesBitmap: kBle=0x02, kWiFiPAF=0x08; all other bits are reserved. |
| // 0x09 = kWiFiPAF(0x08) | reserved(0x01) → contains a reserved bit. |
| // Impl-defined: the BgScanStop spec mandates no specific status for reserved |
| // transport bits. This asserts current behavior. |
| auto result = tester.Invoke(MakeBgScanStopRequest(static_cast<CapabilitiesBitmap>(0x09))); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // kBle is not gated by any Feature bit in the spec; whenever the proxy |
| // advertises kBle in the Transport attribute, ProxyBackGroundScanStopRequest |
| // with kBle passes the cluster-level transport validation and is forwarded to |
| // the transport driver. The mock returns Success, mirroring the WiFiPAF |
| // positive case below. (NotFound for an unrecognised fabric is a |
| // transport-level concern and is covered by the platform transport's own tests.) |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStop_BleValid) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| EXPECT_TRUE(tester.Invoke(MakeBgScanStopRequest(CapabilitiesBitmap::kBle)).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // kWiFiPAF without WI feature SHALL be accepted: the WiFiPAF transport bit is |
| // O.a+, independent of WI. WI only gates the wiFiBands field (absent here). |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStop_WiFiPAFWithoutWIFeature) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan); // no kWiFiNetworkInterface |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| |
| EXPECT_TRUE(tester.Invoke(MakeBgScanStopRequest(CapabilitiesBitmap::kWiFiPAF)).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // wiFiBands with reserved bits SHALL be rejected. |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStop_ReservedWiFiBandBits) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // Bit 1 (0x02) is reserved in WiFiBandBitmap. |
| auto cmd = MakeBgScanStopRequest(CapabilitiesBitmap::kWiFiPAF, |
| chip::MakeOptional(chip::BitMask<WiFiBandBitmap>(static_cast<WiFiBandBitmap>(0x02)))); |
| // Impl-defined: the BgScanStop spec mandates no specific status for reserved |
| // WiFiBand bits. This asserts current behavior. |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // wiFiBands without WI feature is rejected. Impl-defined status: the spec does not |
| // mandate a specific status for a WI-conformance field sent without WI; this asserts |
| // current behavior (INVALID_COMMAND). |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStop_WiFiBandWithoutWIFeature) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan); // no kWiFiNetworkInterface |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| // A band-only stop (transport=0 + wiFiBands) without the WI feature SHALL be |
| // rejected with InvalidCommand by the wiFiBands-requires-WI guard. |
| auto cmd = MakeBgScanStopRequest(static_cast<CapabilitiesBitmap>(0), |
| chip::MakeOptional(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4))); |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // transport=0 with valid wiFiBands and WI feature — SHALL succeed |
| // (a band-only stop is valid per the ProxyBackGroundScanStopRequest behaviour). |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStop_BandOnlyStop_Valid) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| auto cmd = MakeBgScanStopRequest(static_cast<CapabilitiesBitmap>(0), |
| chip::MakeOptional(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4))); |
| EXPECT_TRUE(tester.Invoke(cmd).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Valid kWiFiPAF stop with k5g wiFiBands — SHALL succeed. |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStop_ValidWiFiPAF_5gBand) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| auto cmd = |
| MakeBgScanStopRequest(CapabilitiesBitmap::kWiFiPAF, chip::MakeOptional(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k5g))); |
| EXPECT_TRUE(tester.Invoke(cmd).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Per the ProxyBackGroundScanStopRequest Effect on Receipt: if the NodeId and |
| // FabricId of the client do not match those used in a previous |
| // ProxyBackGroundScanStartRequest, the proxy SHALL take no action and the command |
| // SHALL be rejected with NOT_FOUND. This is transport-side state; the cluster |
| // must propagate the transport's NotFound status. |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStop_TransportNotFound_Propagated) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| // No transport has a matching per-fabric background-scan record. The cluster |
| // fans the stop to every registered transport and reports NotFound only when |
| // none matched, so both mocks must report NotFound. |
| mockBle.SetBgScanStopStatus(Protocols::InteractionModel::Status::NotFound); |
| mockPaf.SetBgScanStopStatus(Protocols::InteractionModel::Status::NotFound); |
| |
| ClusterTester tester(cluster); |
| auto result = tester.Invoke(MakeBgScanStopRequest(CapabilitiesBitmap::kBle)); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::NotFound)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Per the ProxyBackGroundScanStopRequest Effect on Receipt: "If valid Transports and |
| // WiFiBands are received but were not originally requested, the command SHALL return a |
| // status of SUCCESS." The cluster fans the stop to every registered transport and |
| // reports SUCCESS when at least one matched, even if another reports NOT_FOUND. |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStop_PartialMatch_Success) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| // BLE has no matching per-fabric record (NOT_FOUND); PAF reports SUCCESS. |
| mockBle.SetBgScanStopStatus(Protocols::InteractionModel::Status::NotFound); |
| mockPaf.SetBgScanStopStatus(Protocols::InteractionModel::Status::Success); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| EXPECT_TRUE(tester.Invoke(MakeBgScanStopRequest(CapabilitiesBitmap::kBle)).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ============================================================================= |
| // Additional coverage: read-only writes, unsupported transports, scan BUSY, |
| // ProxyMessage TIMEOUT, and CachedResults content. |
| // ============================================================================= |
| |
| // Build a minimal ScanResultStruct for injecting into the ScanCache. |
| static Structs::ScanResultStruct::Type MakeScanEntry(uint16_t discriminator, CapabilitiesBitmap transport) |
| { |
| Structs::ScanResultStruct::Type e; |
| e.transport = chip::BitMask<CapabilitiesBitmap>(transport); |
| e.discriminator = discriminator; |
| e.vendorID = static_cast<chip::VendorId>(0x1234); |
| e.productID = 0x0001; |
| e.address.SetNull(); |
| e.extendedData.SetNull(); |
| return e; |
| } |
| |
| // As above, but on a band: only PAFTP results carry one. |
| static Structs::ScanResultStruct::Type MakePafScanEntry(uint16_t discriminator, WiFiBandBitmap band) |
| { |
| auto e = MakeScanEntry(discriminator, CapabilitiesBitmap::kWiFiPAF); |
| e.wiFiBand.SetValue(chip::BitMask<WiFiBandBitmap>(band)); |
| return e; |
| } |
| |
| // Spec § ProxyBackGroundScanStopRequest: clearing is scoped to "the transports and |
| // bands on which it has stopped scanning", so a band-scoped clear SHALL leave results |
| // from other bands cached. A result with no WiFiBand takes the spec's 2G4 fallback. |
| TEST_F(TestCommissioningProxyCluster, TestCachedResults_ClearIsBandSelective) |
| { |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| |
| ClusterTester tester(cluster); |
| EXPECT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR); |
| |
| auto readNum = [&]() { |
| uint8_t n = 0xFF; |
| EXPECT_EQ(tester.ReadAttribute(CPAttributes::NumCachedResults::Id, n), CHIP_NO_ERROR); |
| return n; |
| }; |
| |
| cluster.ScanCache().Report(MakePafScanEntry(1000, WiFiBandBitmap::k2g4)); |
| cluster.ScanCache().Report(MakePafScanEntry(2000, WiFiBandBitmap::k5g)); |
| cluster.ScanCache().Report(MakeScanEntry(3000, CapabilitiesBitmap::kWiFiPAF)); // no band -> 2G4 |
| cluster.ScanCache().Report(MakeScanEntry(4000, CapabilitiesBitmap::kBle)); |
| EXPECT_EQ(readNum(), 4u); |
| |
| // Stopping 2G4 drops the 2G4 entry and the bandless one that falls back to it; the |
| // 5G entry and the BLE entry are untouched. |
| cluster.ScanCache().ClearTransport(chip::BitMask<CapabilitiesBitmap>(CapabilitiesBitmap::kWiFiPAF), |
| chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| EXPECT_EQ(readNum(), 2u); |
| |
| // A whole-transport clear (bands == 0) takes the remaining PAF entry, not the BLE one. |
| cluster.ScanCache().ClearTransport(chip::BitMask<CapabilitiesBitmap>(CapabilitiesBitmap::kWiFiPAF)); |
| EXPECT_EQ(readNum(), 1u); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A valid transport bit with no registered driver SHALL be rejected with |
| // INVALID_TRANSPORT_TYPE (test plan TC-2.4 step 11). Register only BLE, request PAF. |
| // This is the distinction the two rejection statuses draw: a spec-defined transport with |
| // no driver is InvalidTransportType, whereas an undefined bit is a malformed field and |
| // gets InvalidCommand (see _ReservedTransportBitOnly). The certification tests rely on |
| // it to choose a transport bit for their negative steps. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_UnsupportedTransport_InvalidTransportType) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| cluster.RegisterTransport(mockBle); // only BLE |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| auto result = tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kWiFiPAF)); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidTransportType)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ProxyScanRequest for a valid-but-unregistered transport SHALL be rejected with |
| // INVALID_TRANSPORT_TYPE (test plan TC-2.2 step 9). |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_UnsupportedTransport_InvalidTransportType) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| cluster.RegisterTransport(mockBle); // only BLE |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| Commands::ProxyScanRequest::Type command; |
| command.transport = CapabilitiesBitmap::kWiFiPAF; |
| auto result = tester.Invoke(command); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidTransportType)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // ProxyBackGroundScanStartRequest for a valid-but-unregistered transport SHALL be |
| // rejected with INVALID_TRANSPORT_TYPE (spec + test plan TC-2.3 step 19). |
| TEST_F(TestCommissioningProxyCluster, TestBgScanStart_UnsupportedTransport_InvalidTransportType) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| cluster.RegisterTransport(mockBle); // only BLE |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| auto result = tester.Invoke(MakeBgScanStartRequest(CapabilitiesBitmap::kWiFiPAF)); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::InvalidTransportType)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Every command carrying a Transport field validates it separately, so the four checks can |
| // drift apart — ProxyConnectRequest once answered a reserved bit with InvalidTransportType |
| // while the three scan commands answered InvalidCommand, leaving the same malformed |
| // request with two different meanings depending on which command carried it. A reserved |
| // bit is a malformed field on all four; InvalidTransportType is reserved for a |
| // spec-defined transport the proxy has no driver for (see |
| // _UnsupportedTransport_InvalidTransportType). Assert the four agree. |
| TEST_F(TestCommissioningProxyCluster, TestReservedTransportBitRejectedAlikeByEveryCommand) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| |
| // 0x01 is outside the spec-defined set (kBle=0x02, kWiFiPAF=0x08, kNtl=0x10). Sent |
| // alone so the single-transport rule ProxyConnectRequest applies cannot be what |
| // rejects it, and every command reaches its own reserved-bit check. |
| constexpr auto kReserved = static_cast<CapabilitiesBitmap>(0x01); |
| const auto expected = ClusterStatusCode(Protocols::InteractionModel::Status::InvalidCommand); |
| |
| EXPECT_EQ(tester.Invoke(MakeConnectRequest(kReserved)).GetStatusCode(), expected) << "ProxyConnectRequest"; |
| |
| Commands::ProxyScanRequest::Type scan; |
| scan.transport = kReserved; |
| EXPECT_EQ(tester.Invoke(Commands::ProxyScanRequest::Id, scan).GetStatusCode(), expected) << "ProxyScanRequest"; |
| |
| EXPECT_EQ(tester.Invoke(MakeBgScanStartRequest(kReserved)).GetStatusCode(), expected) << "ProxyBackGroundScanStartRequest"; |
| |
| EXPECT_EQ(tester.Invoke(MakeBgScanStopRequest(kReserved)).GetStatusCode(), expected) << "ProxyBackGroundScanStopRequest"; |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec: a ProxyScanRequest received while one is in progress MAY be answered with |
| // BUSY. This implementation chose the BUSY mechanism; verify it. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_ConcurrentBusy) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| // Leave the first scan in-flight (no synchronous contribution) so the aggregator |
| // stays busy for the second request. |
| mockBle.SetAutoContribute(false); |
| Commands::ProxyScanRequest::Type command; |
| command.transport = CapabilitiesBitmap::kBle; |
| [[maybe_unused]] auto first = tester.Invoke(command); // stays pending |
| |
| auto second = tester.Invoke(command); |
| EXPECT_FALSE(second.IsSuccess()); |
| EXPECT_EQ(second.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::Busy)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // The scan watchdog is the only thing that can end an aggregation whose sub-scan never |
| // reports. If it cannot be armed the command SHALL be rejected rather than started, |
| // because an aggregation with no watchdog would never complete — and the rejection SHALL |
| // roll the aggregation back, or every later scan would be answered BUSY for the lifetime |
| // of the process. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_WatchdogArmFailureRejectsAndRollsBack) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| mockTimer.FailNextStart(); |
| |
| Commands::ProxyScanRequest::Type command; |
| command.transport = CapabilitiesBitmap::kBle; |
| |
| auto result = tester.Invoke(command); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::Failure)); |
| |
| // The aggregation was rolled back, so no watchdog is left armed... |
| EXPECT_EQ(mockTimer.ActiveCount(), 0u); |
| |
| // ... and the next request arms its own watchdog and is accepted, not BUSY. |
| EXPECT_TRUE(tester.Invoke(command).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A driver whose transport connection drops mid-exchange reports it through the session |
| // manager rather than leaving the commissioner waiting for the response timer. The |
| // pending request SHALL be answered with the reported status and its timer released. |
| TEST_F(TestCommissioningProxyCluster, TestProxyMessageRequest_TransportFailureAnswersPending) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| uint16_t sid = OpenSession(tester); |
| |
| mockBle.SetAutoRespond(false); |
| |
| static const uint8_t kMsg[] = { 0xAB }; |
| Commands::ProxyMessageRequest::Type cmd; |
| cmd.sessionID = sid; |
| cmd.responseTimeout = 5; |
| cmd.message.SetNonNull(chip::ByteSpan(kMsg, sizeof(kMsg))); |
| |
| [[maybe_unused]] auto pending = tester.Invoke(cmd); |
| EXPECT_FALSE(tester.GetCommandHandler().HasStatus()); |
| EXPECT_EQ(mockTimer.ActiveCount(), 1u); // the response timer is armed |
| |
| // The BTP/PAFTP link drops: the driver reports the failure for this session. |
| mockBle.FailPendingMessage(sid, Protocols::InteractionModel::Status::Failure); |
| |
| ASSERT_TRUE(tester.GetCommandHandler().HasStatus()); |
| EXPECT_EQ(tester.GetCommandHandler().GetLastStatus().status, ClusterStatusCode(Protocols::InteractionModel::Status::Failure)); |
| EXPECT_EQ(mockTimer.ActiveCount(), 0u); // ... and the response timer went with it |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec § ProxyMessageResponse: "If the ResponseTimeout from the ProxyMessageRequest |
| // expires the TIMEOUT status SHALL be returned." |
| TEST_F(TestCommissioningProxyCluster, TestProxyMessageRequest_ResponseTimeout) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| uint16_t sid = OpenSession(tester); |
| |
| // The commissionee never replies, so the request stays pending and only the session's |
| // response timer can resolve it. |
| mockBle.SetAutoRespond(false); |
| |
| static const uint8_t kMsg[] = { 0xAB }; |
| Commands::ProxyMessageRequest::Type cmd; |
| cmd.sessionID = sid; |
| cmd.responseTimeout = 5; |
| cmd.message.SetNonNull(chip::ByteSpan(kMsg, sizeof(kMsg))); |
| |
| // No answer yet: the cluster holds the handle open waiting for the commissionee. |
| // (Invoke() reports its own error for a command that answers asynchronously, so the |
| // handler is what says whether the commissioner has been answered.) |
| [[maybe_unused]] auto pending = tester.Invoke(cmd); |
| EXPECT_FALSE(tester.GetCommandHandler().HasStatus()); |
| EXPECT_FALSE(tester.GetCommandHandler().HasResponse()); |
| |
| // Advancing past ResponseTimeout fires the timer, which answers with TIMEOUT. |
| mockTimer.AdvanceClock(System::Clock::Seconds16(5)); |
| ASSERT_TRUE(tester.GetCommandHandler().HasStatus()); |
| EXPECT_EQ(tester.GetCommandHandler().GetLastStatus().status, ClusterStatusCode(Protocols::InteractionModel::Status::Timeout)); |
| |
| // The expired request no longer holds the session, so the next one is accepted |
| // rather than rejected with BUSY. |
| mockBle.SetAutoRespond(true); |
| EXPECT_TRUE(tester.Invoke(cmd).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A ProxyBackGroundScanStartRequest naming several transports must not report failure |
| // while leaving one of them scanning: the commissioner would believe nothing started. |
| // Either every requested transport starts, or the command rolls back and fails. |
| TEST_F(TestCommissioningProxyCluster, TestProxyBackgroundScanStart_PartialFailureRollsBack) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| |
| // BLE accepts the background scan; Wi-Fi PAF rejects it. |
| mockPaf.SetBgScanStartStatus(Protocols::InteractionModel::Status::Failure); |
| |
| Commands::ProxyBackGroundScanStartRequest::Type cmd; |
| cmd.transport = chip::BitMask<CapabilitiesBitmap>(CapabilitiesBitmap::kBle, CapabilitiesBitmap::kWiFiPAF); |
| cmd.timeout = 30; |
| |
| EXPECT_FALSE(tester.Invoke(cmd).IsSuccess()); |
| |
| // BLE's scan must have been stopped again, so "failed" means nothing is running. |
| EXPECT_FALSE(mockBle.BgScanRunning()); |
| EXPECT_EQ(mockBle.BgScanStopCount(), 1u); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A sub-scan whose completion callback never fires must not block the aggregator |
| // forever: the watchdog ends the aggregation so later scans are still accepted. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_WatchdogEndsStalledAggregation) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| // The sub-scan starts but never reports, so only the watchdog can finish this. |
| mockBle.SetAutoContribute(false); |
| Commands::ProxyScanRequest::Type command; |
| command.transport = CapabilitiesBitmap::kBle; |
| [[maybe_unused]] auto stalled = tester.Invoke(command); |
| |
| // A second request while the first is still aggregating is rejected. |
| EXPECT_EQ(tester.Invoke(command).GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::Busy)); |
| |
| // ScanMaxTime defaults to 10s and the watchdog adds a 5s margin. |
| mockTimer.AdvanceClock(System::Clock::Seconds16(16)); |
| |
| // The aggregation has been closed out, so a new scan is accepted again. |
| mockBle.SetAutoContribute(true); |
| EXPECT_TRUE(tester.Invoke(command).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // The response timer is what eventually answers a ProxyMessageRequest the commissionee |
| // never replies to. If it cannot be armed the command SHALL be rejected, so the |
| // commissioner gets an answer instead of an exchange that is never resolved. |
| TEST_F(TestCommissioningProxyCluster, TestProxyMessageRequest_ResponseTimerArmFailureRejects) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| uint16_t sid = OpenSession(tester); |
| |
| // The commissionee would not reply on its own, so only the timer could resolve this. |
| mockBle.SetAutoRespond(false); |
| |
| static const uint8_t kMsg[] = { 0xAB }; |
| Commands::ProxyMessageRequest::Type cmd; |
| cmd.sessionID = sid; |
| cmd.responseTimeout = 5; |
| cmd.message.SetNonNull(chip::ByteSpan(kMsg, sizeof(kMsg))); |
| |
| mockTimer.FailNextStart(); |
| |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::Failure)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A removed fabric SHALL leave nothing behind: its sessions are torn down through the |
| // owning transport, and every driver is told to drop its background scans. FabricIndex |
| // values are reused, so residue would be inherited by the next fabric to take the index. |
| TEST_F(TestCommissioningProxyCluster, TestOnFabricRemoved_DropsSessionsAndDriverState) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| tester.SetFabricIndex(1); |
| uint16_t sid = OpenSession(tester); |
| EXPECT_TRUE(cluster.Sessions().FindSession(sid).has_value()); |
| |
| // No FabricTable in unit tests, so drive the delegate callback directly. |
| cluster.OnFabricRemoved(chip::Server::GetInstance().GetFabricTable(), 1); |
| |
| EXPECT_FALSE(cluster.Sessions().FindSession(sid).has_value()); |
| EXPECT_TRUE(cluster.Sessions().IsEmpty()); |
| EXPECT_EQ(mockBle.FabricRemovedCount(), 1u); |
| EXPECT_EQ(mockPaf.FabricRemovedCount(), 1u); |
| EXPECT_EQ(mockBle.LastRemovedFabric(), 1); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Another fabric's session SHALL survive. |
| TEST_F(TestCommissioningProxyCluster, TestOnFabricRemoved_LeavesOtherFabricsAlone) |
| { |
| SKIP_IF_MAX_SESSIONS_BELOW(2); |
| |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| tester.SetFabricIndex(1); |
| uint16_t sid1 = OpenSession(tester); |
| tester.SetFabricIndex(2); |
| uint16_t sid2 = OpenSession(tester); |
| |
| cluster.OnFabricRemoved(chip::Server::GetInstance().GetFabricTable(), 1); |
| |
| EXPECT_FALSE(cluster.Sessions().FindSession(sid1).has_value()); |
| EXPECT_TRUE(cluster.Sessions().FindSession(sid2).has_value()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // CachedResults / NumCachedResults reflect the ScanCache: null/0 when empty, unique |
| // per discriminator/VID/PID/transport (spec), and cleared by ClearTransport. Change |
| // reporting for both attributes is the cluster's responsibility. |
| TEST_F(TestCommissioningProxyCluster, TestCachedResults_ReportDedupAndClear) |
| { |
| BitMask<Feature> bgs(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(bgs), mockTimer); |
| RegisterMocks(cluster); |
| |
| ClusterTester tester(cluster); |
| EXPECT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR); |
| |
| auto readNum = [&]() { |
| uint8_t n = 0xFF; |
| EXPECT_EQ(tester.ReadAttribute(CPAttributes::NumCachedResults::Id, n), CHIP_NO_ERROR); |
| return n; |
| }; |
| |
| // Empty cache: NumCachedResults 0, CachedResults null. |
| EXPECT_EQ(readNum(), 0u); |
| { |
| Attributes::CachedResults::TypeInfo::DecodableType list; |
| ASSERT_EQ(tester.ReadAttribute(CPAttributes::CachedResults::Id, list), CHIP_NO_ERROR); |
| EXPECT_TRUE(list.IsNull()); |
| } |
| |
| // Report device A → count 1; CachedResults and NumCachedResults marked dirty. |
| cluster.ScanCache().Report(MakeScanEntry(1000, CapabilitiesBitmap::kBle)); |
| EXPECT_EQ(readNum(), 1u); |
| EXPECT_TRUE(tester.IsAttributeDirty(CPAttributes::CachedResults::Id)); |
| EXPECT_TRUE(tester.IsAttributeDirty(CPAttributes::NumCachedResults::Id)); |
| |
| // Report device B (different discriminator) → count 2. |
| cluster.ScanCache().Report(MakeScanEntry(2000, CapabilitiesBitmap::kBle)); |
| EXPECT_EQ(readNum(), 2u); |
| |
| // Re-report device A (same discriminator/VID/PID/transport) → dedup, count unchanged. |
| cluster.ScanCache().Report(MakeScanEntry(1000, CapabilitiesBitmap::kBle)); |
| EXPECT_EQ(readNum(), 2u); |
| |
| // CachedResults is now a non-null list with 2 entries. |
| { |
| Attributes::CachedResults::TypeInfo::DecodableType list; |
| ASSERT_EQ(tester.ReadAttribute(CPAttributes::CachedResults::Id, list), CHIP_NO_ERROR); |
| ASSERT_FALSE(list.IsNull()); |
| size_t count = 0; |
| auto it = list.Value().begin(); |
| while (it.Next()) |
| { |
| ++count; |
| } |
| EXPECT_EQ(it.GetStatus(), CHIP_NO_ERROR); |
| EXPECT_EQ(count, 2u); |
| } |
| |
| // Clearing the BLE transport removes all its entries → count 0. |
| cluster.ScanCache().ClearTransport(chip::BitMask<CapabilitiesBitmap>(CapabilitiesBitmap::kBle)); |
| EXPECT_EQ(readNum(), 0u); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec § ProxyBackGroundScanStartRequest: "The proxy SHALL retain each result for |
| // CacheTimeout ... and SHALL remove the result upon expiry." |
| TEST_F(TestCommissioningProxyCluster, TestCachedResults_EntryExpiresAfterCacheTimeout) |
| { |
| BitMask<Feature> bgs(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(bgs), mockTimer); |
| RegisterMocks(cluster); |
| |
| ClusterTester tester(cluster); |
| EXPECT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR); |
| |
| auto readNum = [&]() { |
| uint8_t n = 0xFF; |
| EXPECT_EQ(tester.ReadAttribute(CPAttributes::NumCachedResults::Id, n), CHIP_NO_ERROR); |
| return n; |
| }; |
| |
| // CacheTimeout defaults to 120s. |
| cluster.ScanCache().Report(MakeScanEntry(1000, CapabilitiesBitmap::kBle)); |
| EXPECT_EQ(readNum(), 1u); |
| |
| // Still inside the TTL: the sweep runs but keeps the entry. |
| mockTimer.AdvanceClock(System::Clock::Seconds16(119)); |
| EXPECT_EQ(readNum(), 1u); |
| |
| // Past the TTL: the entry is dropped and both attributes are reported. |
| tester.GetDirtyList().clear(); |
| mockTimer.AdvanceClock(System::Clock::Seconds16(2)); |
| EXPECT_EQ(readNum(), 0u); |
| EXPECT_TRUE(tester.IsAttributeDirty(CPAttributes::CachedResults::Id)); |
| EXPECT_TRUE(tester.IsAttributeDirty(CPAttributes::NumCachedResults::Id)); |
| |
| // Nothing left to sweep, so the cache stops re-arming its timer. |
| EXPECT_EQ(mockTimer.ActiveCount(), 0u); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec § ProxyBackGroundScanStartRequest: "for each discovered device, the proxy SHALL |
| // reset the timer associated to this device on rediscovery of the same device". |
| TEST_F(TestCommissioningProxyCluster, TestCachedResults_RediscoveryResetsTtl) |
| { |
| BitMask<Feature> bgs(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(bgs), mockTimer); |
| RegisterMocks(cluster); |
| |
| ClusterTester tester(cluster); |
| EXPECT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR); |
| |
| auto readNum = [&]() { |
| uint8_t n = 0xFF; |
| EXPECT_EQ(tester.ReadAttribute(CPAttributes::NumCachedResults::Id, n), CHIP_NO_ERROR); |
| return n; |
| }; |
| |
| cluster.ScanCache().Report(MakeScanEntry(1000, CapabilitiesBitmap::kBle)); |
| |
| // Rediscovered at t=100s, which pushes expiry out to t=220s. |
| mockTimer.AdvanceClock(System::Clock::Seconds16(100)); |
| cluster.ScanCache().Report(MakeScanEntry(1000, CapabilitiesBitmap::kBle)); |
| |
| // t=130s is past the ORIGINAL 120s expiry; the refreshed entry SHALL survive. |
| mockTimer.AdvanceClock(System::Clock::Seconds16(30)); |
| EXPECT_EQ(readNum(), 1u); |
| |
| // t=221s is past the refreshed expiry. |
| mockTimer.AdvanceClock(System::Clock::Seconds16(91)); |
| EXPECT_EQ(readNum(), 0u); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Entries with different TTLs expire independently, and the sweep keeps re-arming |
| // while any entry remains. |
| TEST_F(TestCommissioningProxyCluster, TestCachedResults_SweepExpiresOnlyDueEntries) |
| { |
| BitMask<Feature> bgs(Feature::kBackgroundScan); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(bgs), mockTimer); |
| RegisterMocks(cluster); |
| |
| ClusterTester tester(cluster); |
| EXPECT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR); |
| |
| auto readNum = [&]() { |
| uint8_t n = 0xFF; |
| EXPECT_EQ(tester.ReadAttribute(CPAttributes::NumCachedResults::Id, n), CHIP_NO_ERROR); |
| return n; |
| }; |
| |
| cluster.ScanCache().Report(MakeScanEntry(1000, CapabilitiesBitmap::kBle)); // expires t=120 |
| mockTimer.AdvanceClock(System::Clock::Seconds16(60)); |
| cluster.ScanCache().Report(MakeScanEntry(2000, CapabilitiesBitmap::kBle)); // expires t=180 |
| EXPECT_EQ(readNum(), 2u); |
| |
| // t=121: only the first entry is due; the sweep stays armed for the second. |
| mockTimer.AdvanceClock(System::Clock::Seconds16(61)); |
| EXPECT_EQ(readNum(), 1u); |
| EXPECT_EQ(mockTimer.ActiveCount(), 1u); |
| |
| mockTimer.AdvanceClock(System::Clock::Seconds16(60)); |
| EXPECT_EQ(readNum(), 0u); |
| EXPECT_EQ(mockTimer.ActiveCount(), 0u); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A cache destroyed while its TTL sweep is armed must cancel the timer on the way out: |
| // the TimerDelegate holds a pointer to the cache as its TimerContext, and would |
| // dereference freed memory when the sweep came due. |
| TEST_F(TestCommissioningProxyCluster, TestScanCache_DestructorCancelsSweepTimer) |
| { |
| class CacheObserver : public ScanCacheObserver |
| { |
| public: |
| void MarkCachedResultsDirty() override {} |
| uint16_t GetCacheTimeout() const override { return 60; } |
| uint8_t GetMaxCachedResults() const override { return 10; } |
| }; |
| |
| CacheObserver observer; |
| { |
| CommissioningProxyScanCache cache(observer, mockTimer); |
| cache.Report(MakeScanEntry(1000, CapabilitiesBitmap::kBle)); |
| EXPECT_EQ(mockTimer.ActiveCount(), 1u); // the sweep is armed |
| } |
| |
| // Shutdown() was never called, so only the destructor can have cancelled it. |
| EXPECT_EQ(mockTimer.ActiveCount(), 0u); |
| } |
| |
| // ============================================================================= |
| // Additional coverage: fabric isolation on established sessions, session |
| // lifecycle, ProxyMessage edge paths, scan sub-scan handling, GeneratedCommands. |
| // ============================================================================= |
| |
| // Spec: the proxy associates a session with the invoking fabric, and a disconnect |
| // whose fabric does not match SHALL be rejected with NOT_FOUND. Fabric 1 opens a |
| // session; fabric 2 cannot disconnect it, but fabric 1 can. |
| TEST_F(TestCommissioningProxyCluster, TestProxyDisconnectRequest_WrongFabricEstablishedSession_NotFound) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| tester.SetFabricIndex(1); |
| uint16_t sid = OpenSession(tester); |
| |
| Commands::ProxyDisconnectRequest::Type cmd; |
| cmd.sessionID.SetNonNull(sid); |
| |
| // Fabric 2 attempts to disconnect fabric 1's session → NOT_FOUND. |
| tester.SetFabricIndex(2); |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::NotFound)); |
| |
| // The owning fabric can still disconnect it (session survived the foreign attempt). |
| tester.SetFabricIndex(1); |
| EXPECT_TRUE(tester.Invoke(cmd).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec: a ProxyMessageRequest whose fabric does not match the session's fabric SHALL |
| // be rejected with NOT_FOUND. |
| TEST_F(TestCommissioningProxyCluster, TestProxyMessageRequest_WrongFabricEstablishedSession_NotFound) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| tester.SetFabricIndex(1); |
| uint16_t sid = OpenSession(tester); |
| |
| tester.SetFabricIndex(2); |
| static const uint8_t kMsg[] = { 0x01 }; |
| Commands::ProxyMessageRequest::Type cmd; |
| cmd.sessionID = sid; |
| cmd.responseTimeout = 5; |
| cmd.message.SetNonNull(chip::ByteSpan(kMsg, sizeof(kMsg))); |
| |
| auto result = tester.Invoke(cmd); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::NotFound)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec: "The SessionId allows multiple commissioning sessions to be run in parallel." |
| // With MaxSessions >= 2, two connects SHALL each get a distinct, non-zero SessionId. |
| TEST_F(TestCommissioningProxyCluster, TestProxyConnectRequest_MultipleSessionsHaveDistinctSessionIds) |
| { |
| SKIP_IF_MAX_SESSIONS_BELOW(2); |
| |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| auto first = tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kBle)); |
| ASSERT_TRUE(first.IsSuccess()); |
| ASSERT_TRUE(first.response.has_value()); |
| auto second = tester.Invoke(MakeConnectRequest(CapabilitiesBitmap::kBle)); |
| ASSERT_TRUE(second.IsSuccess()); |
| ASSERT_TRUE(second.response.has_value()); |
| |
| if (first.response.has_value() && second.response.has_value()) |
| { |
| EXPECT_NE(first.response->sessionID, 0u); |
| EXPECT_NE(second.response->sessionID, 0u); |
| EXPECT_NE(first.response->sessionID, second.response->sessionID); |
| } |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // A ProxyDisconnectRequest SHALL remove the session: a subsequent ProxyMessageRequest |
| // referencing the same SessionId SHALL be rejected with NOT_FOUND. |
| TEST_F(TestCommissioningProxyCluster, TestProxyDisconnectRequest_RemovesSession) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| uint16_t sid = OpenSession(tester); |
| |
| Commands::ProxyDisconnectRequest::Type dc; |
| dc.sessionID.SetNonNull(sid); |
| EXPECT_TRUE(tester.Invoke(dc).IsSuccess()); |
| |
| static const uint8_t kMsg[] = { 0x01 }; |
| Commands::ProxyMessageRequest::Type msg; |
| msg.sessionID = sid; |
| msg.responseTimeout = 5; |
| msg.message.SetNonNull(chip::ByteSpan(kMsg, sizeof(kMsg))); |
| auto result = tester.Invoke(msg); |
| EXPECT_FALSE(result.IsSuccess()); |
| EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::NotFound)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // Spec § ProxyMessageRequest ResponseTimeout: "A value of zero indicates no response |
| // is expected and the proxy should send ProxyMessageResponse immediately indicating |
| // success." The response message SHALL be null. |
| TEST_F(TestCommissioningProxyCluster, TestProxyMessageRequest_ResponseTimeoutZero_ImmediateSuccess) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| uint16_t sid = OpenSession(tester); |
| |
| static const uint8_t kMsg[] = { 0x01, 0x02 }; |
| Commands::ProxyMessageRequest::Type cmd; |
| cmd.sessionID = sid; |
| cmd.responseTimeout = 0; |
| cmd.message.SetNonNull(chip::ByteSpan(kMsg, sizeof(kMsg))); |
| |
| auto result = tester.Invoke(cmd); |
| ASSERT_TRUE(result.IsSuccess()); |
| ASSERT_TRUE(result.response.has_value()); |
| if (result.response.has_value()) |
| { |
| EXPECT_EQ(result.response->sessionID, sid); |
| EXPECT_TRUE(result.response->message.IsNull()); |
| } |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // If the transport SendMessage fails, the command SHALL fail and the pending state |
| // SHALL be rolled back (the session is not left BUSY for the next request). |
| TEST_F(TestCommissioningProxyCluster, TestProxyMessageRequest_SendMessageFailure_RollsBackPending) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| uint16_t sid = OpenSession(tester); |
| |
| static const uint8_t kMsg[] = { 0xAA }; |
| Commands::ProxyMessageRequest::Type cmd; |
| cmd.sessionID = sid; |
| cmd.responseTimeout = 5; |
| cmd.message.SetNonNull(chip::ByteSpan(kMsg, sizeof(kMsg))); |
| |
| // First request: the transport send fails → FAILURE. |
| mockBle.SetSendMessageError(CHIP_ERROR_INTERNAL); |
| auto first = tester.Invoke(cmd); |
| EXPECT_FALSE(first.IsSuccess()); |
| EXPECT_EQ(first.GetStatusCode(), ClusterStatusCode(Protocols::InteractionModel::Status::Failure)); |
| |
| // The pending state was rolled back: a follow-up request is not rejected as BUSY. |
| mockBle.SetSendMessageError(CHIP_NO_ERROR); |
| EXPECT_TRUE(tester.Invoke(cmd).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // If no requested sub-scan can be started, the ProxyScanRequest SHALL fail rather |
| // than hang waiting for a contribution that never comes. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_AllSubScansFailToStart_Error) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| |
| mockBle.SetScanStatus(Protocols::InteractionModel::Status::Failure); |
| Commands::ProxyScanRequest::Type command; |
| command.transport = CapabilitiesBitmap::kBle; |
| EXPECT_FALSE(tester.Invoke(command).IsSuccess()); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // If some (but not all) requested sub-scans start, the combined ProxyScanResponse |
| // SHALL carry the results of the transports that did start. |
| TEST_F(TestCommissioningProxyCluster, TestProxyScanRequest_PartialStart_ReturnsStartedResults) |
| { |
| TestServerClusterContext context; |
| BitMask<Feature> features(Feature::kWiFiNetworkInterface); |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(features), mockTimer); |
| RegisterMocks(cluster); |
| cluster.SetSupportedWiFiBands(chip::BitMask<WiFiBandBitmap>(WiFiBandBitmap::k2g4)); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester tester(cluster); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kBle); |
| SKIP_IF_TRANSPORT_UNSUPPORTED(tester, CapabilitiesBitmap::kWiFiPAF); |
| |
| // PAF fails to start; BLE succeeds and contributes 2 results. |
| mockPaf.SetScanStatus(Protocols::InteractionModel::Status::Failure); |
| Commands::ProxyScanRequest::Type command; |
| command.transport.Set(CapabilitiesBitmap::kBle); |
| command.transport.Set(CapabilitiesBitmap::kWiFiPAF); |
| |
| auto result = tester.Invoke(command); |
| ASSERT_TRUE(result.IsSuccess()); |
| ASSERT_TRUE(result.response.has_value()); |
| if (result.response.has_value()) |
| { |
| EXPECT_EQ(result.response->numberOfResults, 2u); |
| } |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // GeneratedCommandList SHALL advertise the three response commands the server |
| // generates (regression guard for the GeneratedCommands override). |
| TEST_F(TestCommissioningProxyCluster, TestGeneratedCommandsAdvertisesResponses) |
| { |
| TestServerClusterContext context; |
| CommissioningProxyCluster cluster(kTestEndpointId, CommissioningProxyCluster::Config(BitMask<Feature>{}), mockTimer); |
| RegisterMocks(cluster); |
| EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| ReadOnlyBufferBuilder<CommandId> generated; |
| EXPECT_EQ(cluster.GeneratedCommands(ConcreteClusterPath(kTestEndpointId, CommissioningProxy::Id), generated), CHIP_NO_ERROR); |
| auto buffer = generated.TakeBuffer(); |
| |
| bool hasConnect = false, hasScan = false, hasMessage = false; |
| for (const CommandId id : buffer) |
| { |
| hasConnect |= (id == Commands::ProxyConnectResponse::Id); |
| hasScan |= (id == Commands::ProxyScanResponse::Id); |
| hasMessage |= (id == Commands::ProxyMessageResponse::Id); |
| } |
| EXPECT_TRUE(hasConnect); |
| EXPECT_TRUE(hasScan); |
| EXPECT_TRUE(hasMessage); |
| EXPECT_EQ(buffer.size(), 3u); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| } // namespace |