blob: c25a2490ee98c4e62447e356fd2ff2df5f481197 [file] [log] [blame]
/*
*
* Copyright (c) 2026 Project CHIP Authors
*
* 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 <app/clusters/on-off-server/OnOffCluster.h>
#include <clusters/OnOff/Metadata.h>
#include <lib/support/TimerDelegateMock.h>
#include <pw_unit_test/framework.h>
#include <app/server-cluster/testing/AttributeTesting.h>
#include <app/server-cluster/testing/ClusterTester.h>
#include <app/server-cluster/testing/TestServerClusterContext.h>
#include <app/server-cluster/testing/ValidateGlobalAttributes.h>
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::OnOff;
using namespace chip::Testing;
using chip::Testing::IsAcceptedCommandsListEqualTo;
using chip::Testing::IsAttributesListEqualTo;
namespace {
constexpr EndpointId kTestEndpointId = 1;
class MockOnOffDelegate : public OnOffDelegate
{
public:
bool mOnOff = false;
bool mCalled = false;
bool mStartupCalled = false;
void OnOnOffChanged(bool on) override
{
mOnOff = on;
mCalled = true;
}
void OnOffStartup(bool on) override
{
mOnOff = on;
mStartupCalled = true;
}
};
struct TestOnOffCluster : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { Platform::MemoryShutdown(); }
void SetUp() override
{
mCluster.AddDelegate(&mMockDelegate);
EXPECT_EQ(mCluster.Startup(mClusterTester.GetServerClusterContext()), CHIP_NO_ERROR);
}
void TearDown() override {}
MockOnOffDelegate mMockDelegate;
TimerDelegateMock mMockTimerDelegate;
OnOffCluster mCluster{ kTestEndpointId, { .timerDelegate = mMockTimerDelegate } };
ClusterTester mClusterTester{ mCluster };
};
TEST_F(TestOnOffCluster, TestAttributesList)
{
std::vector<DataModel::AttributeEntry> mandatoryAttributes(Attributes::kMandatoryMetadata.begin(),
Attributes::kMandatoryMetadata.end());
EXPECT_TRUE(IsAttributesListEqualTo(mCluster, mandatoryAttributes));
}
TEST_F(TestOnOffCluster, TestAcceptedCommands)
{
EXPECT_TRUE(IsAcceptedCommandsListEqualTo(mCluster,
{
Commands::Off::kMetadataEntry,
Commands::On::kMetadataEntry,
Commands::Toggle::kMetadataEntry,
}));
}
TEST_F(TestOnOffCluster, TestReadAttributes)
{
bool onOff = true;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::OnOff::Id, onOff), CHIP_NO_ERROR);
EXPECT_FALSE(onOff);
uint16_t revision = 0;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::ClusterRevision::Id, revision), CHIP_NO_ERROR);
EXPECT_EQ(revision, kRevision);
uint32_t featureMap = 1;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_EQ(featureMap, 0u);
}
TEST_F(TestOnOffCluster, TestCommands)
{
// Step 1: Test On Command
EXPECT_TRUE(mClusterTester.Invoke<Commands::On::Type>(Commands::On::Type()).IsSuccess());
EXPECT_TRUE(mMockDelegate.mCalled);
EXPECT_TRUE(mMockDelegate.mOnOff);
mMockDelegate.mCalled = false;
bool onOff = false;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::OnOff::Id, onOff), CHIP_NO_ERROR);
EXPECT_TRUE(onOff);
// Step 2: Test Off Command
EXPECT_TRUE(mClusterTester.Invoke<Commands::Off::Type>(Commands::Off::Type()).IsSuccess());
EXPECT_TRUE(mMockDelegate.mCalled);
EXPECT_FALSE(mMockDelegate.mOnOff);
mMockDelegate.mCalled = false;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::OnOff::Id, onOff), CHIP_NO_ERROR);
EXPECT_FALSE(onOff);
// Step 3: Test Toggle Command (from Off to On)
EXPECT_TRUE(mClusterTester.Invoke<Commands::Toggle::Type>(Commands::Toggle::Type()).IsSuccess());
EXPECT_TRUE(mMockDelegate.mCalled);
EXPECT_TRUE(mMockDelegate.mOnOff);
mMockDelegate.mCalled = false;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::OnOff::Id, onOff), CHIP_NO_ERROR);
EXPECT_TRUE(onOff);
// Step 4: Test Toggle Command (from On to Off)
EXPECT_TRUE(mClusterTester.Invoke<Commands::Toggle::Type>(Commands::Toggle::Type()).IsSuccess());
EXPECT_TRUE(mMockDelegate.mCalled);
EXPECT_FALSE(mMockDelegate.mOnOff);
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::OnOff::Id, onOff), CHIP_NO_ERROR);
EXPECT_FALSE(onOff);
}
TEST_F(TestOnOffCluster, TestNoPersistenceUseDefaultsOnStartup)
{
MockOnOffDelegate mockDelegate;
TimerDelegateMock mockTimerDelegate;
TestServerClusterContext context;
// Step 1: Initial startup, set to ON
{
OnOffCluster cluster(kTestEndpointId, { .timerDelegate = mockTimerDelegate, .defaults = { .onOff = true } });
cluster.AddDelegate(&mockDelegate);
EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR);
chip::Testing::ClusterTester tester(cluster); // Uses its own context
bool onOff = false;
EXPECT_EQ(tester.ReadAttribute(Attributes::OnOff::Id, onOff), CHIP_NO_ERROR);
EXPECT_TRUE(onOff);
EXPECT_TRUE(cluster.GetOnOff());
}
// Step 2:
// - Restart, verify state is NOT persisted (should be default OFF)
// - We reuse the same context so persistence is the same. Only defaults apply.
{
OnOffCluster cluster(kTestEndpointId, { .timerDelegate = mockTimerDelegate, .defaults = { .onOff = false } });
cluster.AddDelegate(&mockDelegate);
EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR);
chip::Testing::ClusterTester tester(cluster); // Uses its own context
bool onOff = true; // Initialize to true to ensure it's changed
EXPECT_EQ(tester.ReadAttribute(Attributes::OnOff::Id, onOff), CHIP_NO_ERROR);
EXPECT_FALSE(onOff);
EXPECT_FALSE(cluster.GetOnOff());
}
}
TEST_F(TestOnOffCluster, TestPersistence)
{
MockOnOffDelegate mockDelegate;
TimerDelegateMock mockTimerDelegate;
TestServerClusterContext context;
// Step 1: Initial startup, set to ON and check persistence
{
OnOffCluster cluster(kTestEndpointId, { .timerDelegate = mockTimerDelegate });
cluster.AddDelegate(&mockDelegate);
EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR);
chip::Testing::ClusterTester tester(cluster);
EXPECT_TRUE(tester.Invoke<Commands::On::Type>(Commands::On::Type()).IsSuccess());
bool onOff = false;
EXPECT_EQ(tester.ReadAttribute(Attributes::OnOff::Id, onOff), CHIP_NO_ERROR);
EXPECT_TRUE(onOff);
// Check that the value is persisted in storage
uint8_t persistedValue = 0;
MutableByteSpan span(&persistedValue, sizeof(persistedValue));
EXPECT_EQ(context.Get().attributeStorage.ReadValue(
ConcreteAttributePath(kTestEndpointId, Clusters::OnOff::Id, Attributes::OnOff::Id), span),
CHIP_NO_ERROR);
EXPECT_TRUE(persistedValue != 0);
}
// Step 2: Restart, verify state IS persisted
{
OnOffCluster cluster(kTestEndpointId, { .timerDelegate = mockTimerDelegate });
cluster.AddDelegate(&mockDelegate);
EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR);
chip::Testing::ClusterTester tester(cluster);
bool onOff = false; // Initialize to false to ensure it's changed
EXPECT_EQ(tester.ReadAttribute(Attributes::OnOff::Id, onOff), CHIP_NO_ERROR);
EXPECT_TRUE(onOff);
}
}
TEST_F(TestOnOffCluster, TestDefaultValue)
{
MockOnOffDelegate mockDelegate;
TimerDelegateMock mockTimerDelegate;
TestServerClusterContext context;
// Test with default false
{
OnOffCluster cluster(kTestEndpointId, { .timerDelegate = mockTimerDelegate, .defaults = { .onOff = false } });
cluster.AddDelegate(&mockDelegate);
EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR);
EXPECT_FALSE(cluster.GetOnOff());
}
// Test with default true
{
OnOffCluster cluster(kTestEndpointId, { .timerDelegate = mockTimerDelegate, .defaults = { .onOff = true } });
cluster.AddDelegate(&mockDelegate);
EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetOnOff());
}
}
struct TestOffOnlyOnOffCluster : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { Platform::MemoryShutdown(); }
void SetUp() override
{
mCluster.AddDelegate(&mMockDelegate);
EXPECT_EQ(mCluster.Startup(mClusterTester.GetServerClusterContext()), CHIP_NO_ERROR);
}
void TearDown() override {}
MockOnOffDelegate mMockDelegate;
TimerDelegateMock mMockTimerDelegate;
OnOffCluster mCluster{ kTestEndpointId,
{ .timerDelegate = mMockTimerDelegate, .featureMap = BitMask<Feature>(Feature::kOffOnly) } };
ClusterTester mClusterTester{ mCluster };
};
TEST_F(TestOffOnlyOnOffCluster, TestFeatureMap)
{
uint32_t featureMap = 0;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_EQ(featureMap, static_cast<uint32_t>(Feature::kOffOnly));
}
TEST_F(TestOffOnlyOnOffCluster, TestAcceptedCommands)
{
EXPECT_TRUE(IsAcceptedCommandsListEqualTo(mCluster, { Commands::Off::kMetadataEntry }));
}
TEST_F(TestOffOnlyOnOffCluster, TestInvokeCommands)
{
EXPECT_TRUE(mClusterTester.Invoke<Commands::Off::Type>(Commands::Off::Type()).IsSuccess());
EXPECT_EQ(mClusterTester.Invoke<Commands::On::Type>(Commands::On::Type()).status,
Protocols::InteractionModel::Status::UnsupportedCommand);
EXPECT_EQ(mClusterTester.Invoke<Commands::Toggle::Type>(Commands::Toggle::Type()).status,
Protocols::InteractionModel::Status::UnsupportedCommand);
}
TEST_F(TestOnOffCluster, TestMultipleDelegates)
{
MockOnOffDelegate secondaryDelegate;
mCluster.AddDelegate(&secondaryDelegate);
// Step 1: On Command - Both delegates should be called
EXPECT_TRUE(mClusterTester.Invoke<Commands::On::Type>(Commands::On::Type()).IsSuccess());
EXPECT_TRUE(mMockDelegate.mCalled);
EXPECT_TRUE(mMockDelegate.mOnOff);
mMockDelegate.mCalled = false;
EXPECT_TRUE(secondaryDelegate.mCalled);
EXPECT_TRUE(secondaryDelegate.mOnOff);
secondaryDelegate.mCalled = false;
// Step 2: Remove secondary delegate
mCluster.RemoveDelegate(&secondaryDelegate);
// Step 3: Off Command - Only primary should be called
EXPECT_TRUE(mClusterTester.Invoke<Commands::Off::Type>(Commands::Off::Type()).IsSuccess());
EXPECT_TRUE(mMockDelegate.mCalled);
EXPECT_FALSE(mMockDelegate.mOnOff);
EXPECT_FALSE(secondaryDelegate.mCalled);
EXPECT_TRUE(secondaryDelegate.mOnOff); // Should remain true (last state)
}
TEST_F(TestOnOffCluster, TestSceneSupport)
{
// Step 1: Turn ON
EXPECT_TRUE(mClusterTester.Invoke<Commands::On::Type>(Commands::On::Type()).IsSuccess());
EXPECT_TRUE(mMockDelegate.mOnOff);
// Step 2: Serialize the current state
uint8_t buffer[128];
MutableByteSpan serializedBytes(buffer);
EXPECT_EQ(mCluster.SerializeSave(kTestEndpointId, Clusters::OnOff::Id, serializedBytes), CHIP_NO_ERROR);
EXPECT_GT(serializedBytes.size(), 0u);
// Step 3: Turn OFF
EXPECT_TRUE(mClusterTester.Invoke<Commands::Off::Type>(Commands::Off::Type()).IsSuccess());
EXPECT_FALSE(mMockDelegate.mOnOff);
// Step 4: Apply the saved scene to restore the ON state
EXPECT_EQ(mCluster.ApplyScene(kTestEndpointId, Clusters::OnOff::Id, serializedBytes, 0), CHIP_NO_ERROR);
EXPECT_TRUE(mMockDelegate.mOnOff);
}
TEST_F(TestOnOffCluster, TestSceneInvalidAttribute)
{
using AttributeValuePair = ScenesManagement::Structs::AttributeValuePairStruct::Type;
// Construct invalid scene data with a wrong Attribute ID.
AttributeValuePair pairs[1];
pairs[0].attributeID = Attributes::OnOff::Id + 1;
pairs[0].valueUnsigned8.SetValue(1);
uint8_t buffer[128];
MutableByteSpan serializedBytes(buffer);
app::DataModel::List<AttributeValuePair> attributeValueList(pairs);
EXPECT_EQ(mCluster.EncodeAttributeValueList(attributeValueList, serializedBytes), CHIP_NO_ERROR);
EXPECT_EQ(mCluster.ApplyScene(kTestEndpointId, Clusters::OnOff::Id, serializedBytes, 0), CHIP_ERROR_INVALID_ARGUMENT);
}
TEST_F(TestOnOffCluster, TestSceneTransition)
{
// Step 1: Start ON
EXPECT_TRUE(mClusterTester.Invoke(Commands::On::Type()).IsSuccess());
EXPECT_TRUE(mMockDelegate.mOnOff);
// Step 2: Serialize the ON state
uint8_t buffer[128];
MutableByteSpan serializedBytes(buffer);
EXPECT_EQ(mCluster.SerializeSave(kTestEndpointId, Clusters::OnOff::Id, serializedBytes), CHIP_NO_ERROR);
// Step 3: Turn OFF
EXPECT_TRUE(mClusterTester.Invoke(Commands::Off::Type()).IsSuccess());
EXPECT_FALSE(mMockDelegate.mOnOff);
// Step 4: Apply Scene to restore ON state with a 1000ms transition
EXPECT_EQ(mCluster.ApplyScene(kTestEndpointId, Clusters::OnOff::Id, serializedBytes, 1000), CHIP_NO_ERROR);
// OnOff should still be FALSE as the transition is in progress.
EXPECT_FALSE(mMockDelegate.mOnOff);
// Step 5: Advance time to complete the transition
mMockTimerDelegate.AdvanceClock(System::Clock::Milliseconds64(1000));
EXPECT_TRUE(mMockDelegate.mOnOff);
}
TEST_F(TestOnOffCluster, TestSceneTransitionCancellation)
{
// Step 1: Start OFF
EXPECT_TRUE(mClusterTester.Invoke(Commands::Off::Type()).IsSuccess());
EXPECT_FALSE(mMockDelegate.mOnOff);
// Step 2: Prepare a scene that would turn the light ON.
uint8_t buffer[128];
MutableByteSpan serializedBytes(buffer);
EXPECT_EQ(mCluster.SetOnOff(true), CHIP_NO_ERROR); // Manually set to save ON
EXPECT_EQ(mCluster.SerializeSave(kTestEndpointId, Clusters::OnOff::Id, serializedBytes), CHIP_NO_ERROR);
EXPECT_EQ(mCluster.SetOnOff(false), CHIP_NO_ERROR); // Revert to OFF
// Step 3: Apply the scene with a 1000ms transition.
EXPECT_EQ(mCluster.ApplyScene(kTestEndpointId, Clusters::OnOff::Id, serializedBytes, 1000), CHIP_NO_ERROR);
EXPECT_FALSE(mMockDelegate.mOnOff);
EXPECT_TRUE(mCluster.IsSceneTransitionPending());
// Step 4: Send an "Off" Command, which should cancel the ongoing transition.
EXPECT_TRUE(mClusterTester.Invoke(Commands::Off::Type()).IsSuccess());
EXPECT_FALSE(mCluster.IsSceneTransitionPending());
// Step 5: Advance time beyond the original transition time.
mMockTimerDelegate.AdvanceClock(System::Clock::Milliseconds64(1000));
// The state should remain OFF because the transition was cancelled.
EXPECT_FALSE(mMockDelegate.mOnOff);
}
} // namespace