blob: a23e222c67d0c436eb31adbd53e6645507050b36 [file]
/*
* 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-common/zap-generated/callback.h>
#include <app-common/zap-generated/cluster-objects.h>
#include <app/AttributeAccessInterfaceRegistry.h>
#include <app/AttributeValueEncoder.h>
#include <app/EventManagement.h>
#include <app/MessageDef/AttributeReportIBs.h>
#include <app/clusters/account-login-server/account-login-delegate.h>
#include <app/clusters/account-login-server/account-login-server.h>
#include <app/server-cluster/testing/MockCommandHandler.h>
#include <app/tests/test-ember-api.h>
#include <lib/support/CHIPCounter.h>
#include <lib/support/CHIPMem.h>
#include <protocols/interaction_model/StatusCode.h>
#include <pw_unit_test/framework.h>
// Not declared in any header included above -- forward-declared here the same way
// TestChimeClusterBackwardsCompatibility.cpp does for its cluster's init callback.
void MatterAccountLoginPluginServerInitCallback();
void MatterAccountLoginPluginServerShutdownCallback();
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters::AccountLogin;
using chip::Protocols::InteractionModel::Status;
namespace {
constexpr EndpointId kTestEndpointId = 1;
// EventManagement backing storage, so the LoggedOut event (Critical priority) can actually be
// logged; needed to observe whether emberAfAccountLoginClusterLogoutCallback fires it or not.
uint8_t gDebugEventBuffer[128];
uint8_t gInfoEventBuffer[128];
uint8_t gCritEventBuffer[128];
chip::app::CircularEventBuffer gCircularEventBuffer[3];
class FakeAccountLoginDelegate : public Delegate
{
public:
void SetSetupPin(char *) override {}
bool HandleLogin(const CharSpan &, const CharSpan &, const chip::Optional<NodeId> &) override { return mLoginResult; }
bool HandleLogout(const chip::Optional<NodeId> &) override { return mLogoutResult; }
void HandleGetSetupPin(CommandResponseHelper<Commands::GetSetupPINResponse::Type> & helper, const CharSpan &) override
{
Commands::GetSetupPINResponse::Type response;
response.setupPIN = mSetupPin;
TEMPORARY_RETURN_IGNORED helper.Success(response);
}
void GetSetupPin(char *, size_t, const CharSpan &) override {}
void HandleGetDeviceAuthURI(CommandResponseHelper<Commands::GetDeviceAuthURIResponse::Type> & helper) override
{
Commands::GetDeviceAuthURIResponse::Type response;
response.userCode = mUserCode;
response.verificationURI = mVerificationURI;
response.expiresIn = 1800;
response.interval = 5;
TEMPORARY_RETURN_IGNORED helper.Success(response);
}
bool GetOAuthLoggedIn(EndpointId) override { return mOAuthLoggedIn; }
uint16_t GetClusterRevision(EndpointId) override { return mClusterRevision; }
bool mLoginResult = true;
bool mLogoutResult = true;
bool mOAuthLoggedIn = false;
uint16_t mClusterRevision = 3;
CharSpan mSetupPin = "1234"_span;
CharSpan mUserCode = "ABCD-EFGH"_span;
CharSpan mVerificationURI = "https://example.com/device"_span;
};
class TestAccountLoginCluster : public ::testing::Test
{
public:
static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); }
void SetUp() override
{
chip::Testing::numEndpoints = kTestEndpointId + 1;
SetDefaultDelegate(kTestEndpointId, &mDelegate);
MatterAccountLoginPluginServerInitCallback();
const chip::app::LogStorageResources logStorageResources[] = {
{ &gDebugEventBuffer[0], sizeof(gDebugEventBuffer), chip::app::PriorityLevel::Debug },
{ &gInfoEventBuffer[0], sizeof(gInfoEventBuffer), chip::app::PriorityLevel::Info },
{ &gCritEventBuffer[0], sizeof(gCritEventBuffer), chip::app::PriorityLevel::Critical },
};
ASSERT_EQ(mEventCounter.Init(0), CHIP_NO_ERROR);
chip::app::EventManagement::CreateEventManagement(nullptr, MATTER_ARRAY_SIZE(logStorageResources), gCircularEventBuffer,
logStorageResources, &mEventCounter);
}
void TearDown() override
{
chip::app::EventManagement::DestroyEventManagement();
MatterAccountLoginPluginServerShutdownCallback();
SetDefaultDelegate(kTestEndpointId, nullptr);
}
protected:
FakeAccountLoginDelegate mDelegate;
chip::MonotonicallyIncreasingCounter<chip::EventNumber> mEventCounter;
};
ConcreteCommandPath MakeCommandPath(CommandId commandId)
{
return ConcreteCommandPath(kTestEndpointId, Id, commandId);
}
// Reads a scalar attribute through the cluster's real, registered AttributeAccessInterface --
// exercising AccountLoginAttrAccess::Read's attribute-ID dispatch, not just the delegate hook it
// eventually calls into.
template <typename T>
CHIP_ERROR ReadScalarAttributeViaAttrAccess(EndpointId endpoint, AttributeId attributeId, T & outValue)
{
AttributeAccessInterface * attrAccess = AttributeAccessInterfaceRegistry::Instance().Get(endpoint, Id);
VerifyOrReturnError(attrAccess != nullptr, CHIP_ERROR_NOT_FOUND);
uint8_t buf[128];
TLV::TLVWriter writer;
writer.Init(buf);
AttributeReportIBs::Builder builder;
ReturnErrorOnFailure(builder.Init(&writer));
ConcreteAttributePath path(endpoint, Id, attributeId);
ConcreteReadAttributePath readPath(path);
chip::DataVersion dataVersion(0);
Access::SubjectDescriptor subjectDescriptor;
AttributeValueEncoder encoder(builder, subjectDescriptor, path, dataVersion);
ReturnErrorOnFailure(attrAccess->Read(readPath, encoder));
builder.EndOfContainer();
ReturnErrorOnFailure(writer.Finalize());
TLV::TLVReader reader;
reader.Init(buf, writer.GetLengthWritten());
TLV::TLVReader attrReportsReader;
TLV::TLVReader attrReportReader;
TLV::TLVReader attrDataReader;
ReturnErrorOnFailure(reader.Next());
ReturnErrorOnFailure(reader.OpenContainer(attrReportsReader));
ReturnErrorOnFailure(attrReportsReader.Next());
ReturnErrorOnFailure(attrReportsReader.OpenContainer(attrReportReader));
ReturnErrorOnFailure(attrReportReader.Next());
ReturnErrorOnFailure(attrReportReader.OpenContainer(attrDataReader));
// Skip to the Data tag (context tag 2) within AttributeDataIB.
ReturnErrorOnFailure(attrDataReader.Next());
while (!(TLV::IsContextTag(attrDataReader.GetTag()) && TLV::TagNumFromTag(attrDataReader.GetTag()) == 2))
{
ReturnErrorOnFailure(attrDataReader.Next());
}
return attrDataReader.Get(outValue);
}
} // namespace
// -----------------------------------------------------------------------------
// GetSetupPIN
TEST_F(TestAccountLoginCluster, GetSetupPinSucceeds)
{
mDelegate.mSetupPin = "5678"_span;
Commands::GetSetupPIN::DecodableType request;
request.tempAccountIdentifier = "account-1"_span;
chip::Testing::MockCommandHandler mockHandler;
auto commandPath = MakeCommandPath(Commands::GetSetupPIN::Id);
emberAfAccountLoginClusterGetSetupPINCallback(&mockHandler, commandPath, request);
ASSERT_TRUE(mockHandler.HasResponse());
Commands::GetSetupPINResponse::DecodableType response;
EXPECT_EQ(mockHandler.DecodeResponse(response), CHIP_NO_ERROR);
EXPECT_TRUE(response.setupPIN.data_equal("5678"_span));
EXPECT_FALSE(mockHandler.HasStatus());
}
TEST_F(TestAccountLoginCluster, GetSetupPinWithNoDelegateFails)
{
SetDefaultDelegate(kTestEndpointId, nullptr);
Commands::GetSetupPIN::DecodableType request;
request.tempAccountIdentifier = "account-1"_span;
chip::Testing::MockCommandHandler mockHandler;
auto commandPath = MakeCommandPath(Commands::GetSetupPIN::Id);
emberAfAccountLoginClusterGetSetupPINCallback(&mockHandler, commandPath, request);
ASSERT_TRUE(mockHandler.HasStatus());
EXPECT_EQ(mockHandler.GetLastStatus().status.GetStatus(), Status::Failure);
EXPECT_FALSE(mockHandler.HasResponse());
}
// -----------------------------------------------------------------------------
// GetDeviceAuthURI
TEST_F(TestAccountLoginCluster, GetDeviceAuthUriSucceeds)
{
Commands::GetDeviceAuthURI::DecodableType request;
chip::Testing::MockCommandHandler mockHandler;
auto commandPath = MakeCommandPath(Commands::GetDeviceAuthURI::Id);
emberAfAccountLoginClusterGetDeviceAuthURICallback(&mockHandler, commandPath, request);
ASSERT_TRUE(mockHandler.HasResponse());
Commands::GetDeviceAuthURIResponse::DecodableType response;
EXPECT_EQ(mockHandler.DecodeResponse(response), CHIP_NO_ERROR);
EXPECT_TRUE(response.userCode.data_equal("ABCD-EFGH"_span));
EXPECT_TRUE(response.verificationURI.data_equal("https://example.com/device"_span));
EXPECT_EQ(response.interval, 5);
EXPECT_FALSE(mockHandler.HasStatus());
}
TEST_F(TestAccountLoginCluster, GetDeviceAuthUriWithNoDelegateFails)
{
SetDefaultDelegate(kTestEndpointId, nullptr);
Commands::GetDeviceAuthURI::DecodableType request;
chip::Testing::MockCommandHandler mockHandler;
auto commandPath = MakeCommandPath(Commands::GetDeviceAuthURI::Id);
emberAfAccountLoginClusterGetDeviceAuthURICallback(&mockHandler, commandPath, request);
ASSERT_TRUE(mockHandler.HasStatus());
EXPECT_EQ(mockHandler.GetLastStatus().status.GetStatus(), Status::Failure);
EXPECT_FALSE(mockHandler.HasResponse());
}
// -----------------------------------------------------------------------------
// Login
TEST_F(TestAccountLoginCluster, LoginSucceedsMapsToSuccess)
{
mDelegate.mLoginResult = true;
Commands::Login::DecodableType request;
request.tempAccountIdentifier = "account-1"_span;
request.setupPIN = "1234"_span;
chip::Testing::MockCommandHandler mockHandler;
auto commandPath = MakeCommandPath(Commands::Login::Id);
emberAfAccountLoginClusterLoginCallback(&mockHandler, commandPath, request);
ASSERT_TRUE(mockHandler.HasStatus());
EXPECT_EQ(mockHandler.GetLastStatus().status.GetStatus(), Status::Success);
}
TEST_F(TestAccountLoginCluster, LoginFailureMapsToUnsupportedAccess)
{
mDelegate.mLoginResult = false;
Commands::Login::DecodableType request;
request.tempAccountIdentifier = "account-1"_span;
request.setupPIN = "wrong"_span;
chip::Testing::MockCommandHandler mockHandler;
auto commandPath = MakeCommandPath(Commands::Login::Id);
emberAfAccountLoginClusterLoginCallback(&mockHandler, commandPath, request);
ASSERT_TRUE(mockHandler.HasStatus());
EXPECT_EQ(mockHandler.GetLastStatus().status.GetStatus(), Status::UnsupportedAccess);
}
TEST_F(TestAccountLoginCluster, LoginWithNoDelegateFails)
{
SetDefaultDelegate(kTestEndpointId, nullptr);
Commands::Login::DecodableType request;
request.tempAccountIdentifier = "account-1"_span;
request.setupPIN = "1234"_span;
chip::Testing::MockCommandHandler mockHandler;
auto commandPath = MakeCommandPath(Commands::Login::Id);
emberAfAccountLoginClusterLoginCallback(&mockHandler, commandPath, request);
ASSERT_TRUE(mockHandler.HasStatus());
EXPECT_EQ(mockHandler.GetLastStatus().status.GetStatus(), Status::Failure);
}
// -----------------------------------------------------------------------------
// Logout
//
// The current implementation logs the LoggedOut event whenever `node` is present,
// regardless of whether HandleLogout succeeded -- these tests pin down that behavior
// explicitly since it's easy to regress silently (event firing on a failure path).
TEST_F(TestAccountLoginCluster, LogoutSucceedsAndFiresEventWhenNodePresent)
{
mDelegate.mLogoutResult = true;
Commands::Logout::DecodableType request;
request.node = chip::MakeOptional(static_cast<NodeId>(1234));
EventNumber before = EventManagement::GetInstance().GetLastEventNumber();
chip::Testing::MockCommandHandler mockHandler;
auto commandPath = MakeCommandPath(Commands::Logout::Id);
emberAfAccountLoginClusterLogoutCallback(&mockHandler, commandPath, request);
ASSERT_TRUE(mockHandler.HasStatus());
EXPECT_EQ(mockHandler.GetLastStatus().status.GetStatus(), Status::Success);
EXPECT_GT(EventManagement::GetInstance().GetLastEventNumber(), before);
}
TEST_F(TestAccountLoginCluster, LogoutFailureStillFiresEventWhenNodePresent)
{
mDelegate.mLogoutResult = false;
Commands::Logout::DecodableType request;
request.node = chip::MakeOptional(static_cast<NodeId>(1234));
EventNumber before = EventManagement::GetInstance().GetLastEventNumber();
chip::Testing::MockCommandHandler mockHandler;
auto commandPath = MakeCommandPath(Commands::Logout::Id);
emberAfAccountLoginClusterLogoutCallback(&mockHandler, commandPath, request);
ASSERT_TRUE(mockHandler.HasStatus());
EXPECT_EQ(mockHandler.GetLastStatus().status.GetStatus(), Status::Failure);
// Event fires even though the logout itself failed -- current (arguably surprising) behavior.
EXPECT_GT(EventManagement::GetInstance().GetLastEventNumber(), before);
}
TEST_F(TestAccountLoginCluster, LogoutWithoutNodeDoesNotFireEvent)
{
mDelegate.mLogoutResult = true;
Commands::Logout::DecodableType request;
// node intentionally omitted.
EventNumber before = EventManagement::GetInstance().GetLastEventNumber();
chip::Testing::MockCommandHandler mockHandler;
auto commandPath = MakeCommandPath(Commands::Logout::Id);
emberAfAccountLoginClusterLogoutCallback(&mockHandler, commandPath, request);
ASSERT_TRUE(mockHandler.HasStatus());
EXPECT_EQ(mockHandler.GetLastStatus().status.GetStatus(), Status::Success);
EXPECT_EQ(EventManagement::GetInstance().GetLastEventNumber(), before);
}
TEST_F(TestAccountLoginCluster, LogoutWithNoDelegateFails)
{
SetDefaultDelegate(kTestEndpointId, nullptr);
Commands::Logout::DecodableType request;
request.node = chip::MakeOptional(static_cast<NodeId>(1234));
chip::Testing::MockCommandHandler mockHandler;
auto commandPath = MakeCommandPath(Commands::Logout::Id);
emberAfAccountLoginClusterLogoutCallback(&mockHandler, commandPath, request);
ASSERT_TRUE(mockHandler.HasStatus());
EXPECT_EQ(mockHandler.GetLastStatus().status.GetStatus(), Status::Failure);
}
// -----------------------------------------------------------------------------
// Attribute reads
TEST_F(TestAccountLoginCluster, ReadClusterRevision)
{
mDelegate.mClusterRevision = 3;
uint16_t clusterRevision = 0;
EXPECT_EQ(ReadScalarAttributeViaAttrAccess(kTestEndpointId, Attributes::ClusterRevision::Id, clusterRevision), CHIP_NO_ERROR);
EXPECT_EQ(clusterRevision, 3);
}
TEST_F(TestAccountLoginCluster, ReadOAuthLoggedIn)
{
mDelegate.mOAuthLoggedIn = true;
bool loggedIn = false;
EXPECT_EQ(ReadScalarAttributeViaAttrAccess(kTestEndpointId, Attributes::OAuthLoggedIn::Id, loggedIn), CHIP_NO_ERROR);
EXPECT_TRUE(loggedIn);
mDelegate.mOAuthLoggedIn = false;
EXPECT_EQ(ReadScalarAttributeViaAttrAccess(kTestEndpointId, Attributes::OAuthLoggedIn::Id, loggedIn), CHIP_NO_ERROR);
EXPECT_FALSE(loggedIn);
}