blob: b63ef933f5c9dba704f5a00085dcfc9fe8071744 [file] [log] [blame]
#include <pw_unit_test/framework.h>
#include <controller/CommissioningDelegate.h>
#include <cstring>
#include <lib/support/CHIPMem.h>
#include <lib/support/Span.h>
using namespace chip;
using namespace chip::Controller;
namespace {
class CommissioningDelegateTest : public ::testing::Test
{
// This test fixture is currently empty, but can be extended in the future.
};
struct TestDADelegate : public Credentials::DeviceAttestationDelegate
{
// Return a fixed timeout to make behavior observable in tests
Optional<uint16_t> FailSafeExpiryTimeoutSecs() const override { return MakeOptional<uint16_t>(uint16_t{ 42 }); }
// Required pure virtual. We won’t invoke it in this test, but it must exist.
void OnDeviceAttestationCompleted(Controller::DeviceCommissioner * /*deviceCommissioner*/, DeviceProxy * /*device*/,
const Credentials::DeviceAttestationVerifier::AttestationDeviceInfo & /*info*/,
Credentials::AttestationVerificationResult /*attestationResult*/) override
{
// no-op
}
// Override to return true so we can assert a meaningful value
bool ShouldWaitAfterDeviceAttestation() override { return true; }
};
// ---------- StageToString (and MetricKey… if enabled) ----------
TEST_F(CommissioningDelegateTest, StageToString_UnknownAndKnown)
{
// Unknown/default → "???"
EXPECT_STREQ(StageToString(static_cast<CommissioningStage>(250)), "???");
// One representative known stage → non-empty label and not "???"
const char * s = StageToString(kSecurePairing);
ASSERT_NE(s, nullptr);
EXPECT_STRNE(s, "???");
EXPECT_GT(std::strlen(s), 0u);
}
#if MATTER_TRACING_ENABLED
TEST_F(CommissioningDelegateTest, MetricKeyForCommissioningStage_UnknownAndKnown)
{
// Unknown/default → exact, stable fallback
EXPECT_STREQ(MetricKeyForCommissioningStage(static_cast<CommissioningStage>(250)), "core_commissioning_stage_unknown");
// One representative known stage → non-empty, not "unknown", and has the standard prefix
const char * key = MetricKeyForCommissioningStage(kSecurePairing);
ASSERT_NE(key, nullptr);
EXPECT_GT(std::strlen(key), 0u);
EXPECT_STRNE(key, "core_commissioning_stage_unknown");
const char * kPrefix = "core_commissioning_stage_";
EXPECT_EQ(std::strncmp(key, kPrefix, std::strlen(kPrefix)), 0)
<< "Expected metric key to use the standard 'core_commissioning_stage_' prefix.";
}
#endif
// ---------- CommissioningParameters getters/setters & buffer-clearing ----------
TEST_F(CommissioningDelegateTest, CommissioningParameters_DefaultsAndSettersExerciseUncovered)
{
CommissioningParameters p;
// Defaults (a few)
EXPECT_FALSE(p.GetFailsafeTimerSeconds().HasValue());
EXPECT_FALSE(p.GetDeviceRegulatoryLocation().HasValue());
EXPECT_FALSE(p.GetSupportsConcurrentConnection().HasValue());
EXPECT_FALSE(p.GetAttemptWiFiNetworkScan().HasValue());
EXPECT_FALSE(p.GetAttemptThreadNetworkScan().HasValue());
EXPECT_FALSE(p.GetSkipCommissioningComplete().HasValue());
EXPECT_FALSE(p.GetICDCheckInNodeId().HasValue());
EXPECT_FALSE(p.GetICDMonitoredSubject().HasValue());
EXPECT_FALSE(p.GetICDSymmetricKey().HasValue());
EXPECT_FALSE(p.GetICDClientType().HasValue());
EXPECT_FALSE(p.GetICDStayActiveDurationMsec().HasValue());
EXPECT_EQ(p.GetICDRegistrationStrategy(), ICDRegistrationStrategy::kIgnore);
EXPECT_FALSE(p.GetDefaultNTP().HasValue());
EXPECT_EQ(p.GetExtraReadPaths().size(), size_t{ 0 });
// Set timers, regulatory, supports concurrent
p.SetFailsafeTimerSeconds(45);
p.SetCASEFailsafeTimerSeconds(30);
p.SetDeviceRegulatoryLocation(app::Clusters::GeneralCommissioning::RegulatoryLocationTypeEnum::kIndoor);
p.SetSupportsConcurrentConnection(true);
EXPECT_EQ(p.GetFailsafeTimerSeconds().Value(), 45);
EXPECT_EQ(p.GetCASEFailsafeTimerSeconds().Value(), 30);
EXPECT_TRUE(p.GetSupportsConcurrentConnection().Value());
// Country code + T&C ack
p.SetCountryCode("US"_span);
TermsAndConditionsAcknowledgement tca{ .acceptedTermsAndConditions = 1, .acceptedTermsAndConditionsVersion = 2 };
p.SetTermsAndConditionsAcknowledgement(tca);
ASSERT_TRUE(p.GetCountryCode().HasValue());
EXPECT_TRUE(p.GetCountryCode().Value().data_equal("US"_span));
ASSERT_TRUE(p.GetTermsAndConditionsAcknowledgement().HasValue());
EXPECT_EQ(p.GetTermsAndConditionsAcknowledgement().Value().acceptedTermsAndConditions, 1);
// TimeZone & DST lists (single item is fine)
app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type tz{};
tz.offset = 1800;
tz.validAt = 123;
tz.name.SetValue("AMT"_span);
app::DataModel::List<decltype(tz)> tzList(&tz, 1);
p.SetTimeZone(tzList);
ASSERT_TRUE(p.GetTimeZone().HasValue());
EXPECT_EQ(p.GetTimeZone().Value().size(), size_t{ 1 });
app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type dst{};
dst.offset = 3600;
dst.validStarting = static_cast<uint64_t>(50);
dst.validUntil = static_cast<uint64_t>(200);
app::DataModel::List<decltype(dst)> dstList(&dst, 1);
p.SetDSTOffsets(dstList);
ASSERT_TRUE(p.GetDSTOffsets().HasValue());
EXPECT_EQ(p.GetDSTOffsets().Value().size(), size_t{ 1 });
// Default NTP: set value then null
p.SetDefaultNTP(app::DataModel::MakeNullable("pool.ntp.org"_span));
ASSERT_TRUE(p.GetDefaultNTP().HasValue());
EXPECT_FALSE(p.GetDefaultNTP().Value().IsNull());
EXPECT_TRUE(p.GetDefaultNTP().Value().Value().data_equal("pool.ntp.org"_span));
p.SetDefaultNTP(app::DataModel::Nullable<CharSpan>{}); // null
ASSERT_TRUE(p.GetDefaultNTP().HasValue());
EXPECT_TRUE(p.GetDefaultNTP().Value().IsNull());
// Trusted time source: just set a null Nullable to exercise setter/getter
app::DataModel::Nullable<app::Clusters::TimeSynchronization::Structs::FabricScopedTrustedTimeSourceStruct::Type> tts;
p.SetTrustedTimeSource(tts);
ASSERT_TRUE(p.GetTrustedTimeSource().HasValue());
EXPECT_TRUE(p.GetTrustedTimeSource().Value().IsNull());
// Nonces (use spec/SDK lengths, pass explicit sizes) ----
std::array<uint8_t, kCSRNonceLength> csr{};
std::array<uint8_t, kAttestationNonceLength> att{};
// Fill with deterministic non-zero so we avoid relying on zero-init content
std::fill(csr.begin(), csr.end(), 0xA5);
std::fill(att.begin(), att.end(), 0x5A);
p.SetCSRNonce(ByteSpan{ csr });
p.SetAttestationNonce(ByteSpan{ att });
EXPECT_TRUE(p.GetCSRNonce().HasValue());
EXPECT_TRUE(p.GetAttestationNonce().HasValue());
// WiFi + Thread datasets set scans to false
const uint8_t ssid[] = { 'A', 'P' }; // SSID 1–32 bytes
const uint8_t pwd[] = { '2', '4', '4', '4', '6', '6', '6', '6', '6' }; // >= 8 chars for WPA2
p.SetWiFiCredentials(WiFiCredentials{ ByteSpan{ ssid }, ByteSpan{ pwd } });
EXPECT_TRUE(p.GetWiFiCredentials().HasValue());
ASSERT_TRUE(p.GetAttemptWiFiNetworkScan().HasValue());
EXPECT_FALSE(p.GetAttemptWiFiNetworkScan().Value());
const uint8_t tds[] = { 0x07, 0x07, 0x07 }; // any opaque dataset bytes ok for exercising API
p.SetThreadOperationalDataset(ByteSpan{ tds });
EXPECT_TRUE(p.GetThreadOperationalDataset().HasValue());
ASSERT_TRUE(p.GetAttemptThreadNetworkScan().HasValue());
EXPECT_FALSE(p.GetAttemptThreadNetworkScan().Value());
// AttemptThreadNetworkScan should be ignored once dataset is present
p.SetAttemptThreadNetworkScan(true);
ASSERT_TRUE(p.GetAttemptThreadNetworkScan().HasValue());
EXPECT_FALSE(p.GetAttemptThreadNetworkScan().Value());
// NOCChain params + certs + IDs (exercise getters)
NOCChainGenerationParameters ngp{ ByteSpan{ csr }, ByteSpan{ att } };
p.SetNOCChainGenerationParameters(ngp);
p.SetRootCert(ByteSpan{ csr });
p.SetNoc(ByteSpan{ att });
p.SetIcac(ByteSpan{ csr });
// IdentityProtectionKey: provide 16 bytes (AES-128-sized)
std::array<uint8_t, 16> ipkBytes{};
p.SetIpk(ipkBytes);
p.SetAdminSubject(NodeId{ 0x1111'2222'3333'4444ULL });
EXPECT_TRUE(p.GetNOCChainGenerationParameters().HasValue());
EXPECT_TRUE(p.GetRootCert().HasValue());
EXPECT_TRUE(p.GetNoc().HasValue());
EXPECT_TRUE(p.GetIcac().HasValue());
EXPECT_TRUE(p.GetIpk().HasValue());
EXPECT_TRUE(p.GetAdminSubject().HasValue());
// Remote info + default regulatory/location capability
p.SetRemoteVendorId(VendorId::Common);
p.SetRemoteProductId(1234);
p.SetDefaultRegulatoryLocation(app::Clusters::GeneralCommissioning::RegulatoryLocationTypeEnum::kIndoorOutdoor);
p.SetLocationCapability(app::Clusters::GeneralCommissioning::RegulatoryLocationTypeEnum::kIndoorOutdoor);
EXPECT_TRUE(p.GetRemoteVendorId().HasValue());
EXPECT_TRUE(p.GetRemoteProductId().HasValue());
EXPECT_TRUE(p.GetDefaultRegulatoryLocation().HasValue());
EXPECT_TRUE(p.GetLocationCapability().HasValue());
// Non-null path using our concrete no-op delegate
TestDADelegate delegate;
p.SetDeviceAttestationDelegate(&delegate);
ASSERT_NE(p.GetDeviceAttestationDelegate(), nullptr);
EXPECT_EQ(p.GetDeviceAttestationDelegate(), &delegate);
// Call a couple of virtuals to ensure vtable is correct and behavior is observable
auto * d = p.GetDeviceAttestationDelegate();
ASSERT_NE(d, nullptr);
EXPECT_TRUE(d->ShouldWaitAfterDeviceAttestation());
const auto timeout = d->FailSafeExpiryTimeoutSecs();
ASSERT_TRUE(timeout.HasValue());
EXPECT_EQ(timeout.Value(), 42);
// SkipCommissioningComplete + CheckForMatchingFabric flags
p.SetSkipCommissioningComplete(true);
p.SetCheckForMatchingFabric(true);
ASSERT_TRUE(p.GetSkipCommissioningComplete().HasValue());
EXPECT_TRUE(p.GetSkipCommissioningComplete().Value());
EXPECT_TRUE(p.GetCheckForMatchingFabric());
// ICD knobs
p.SetICDRegistrationStrategy(ICDRegistrationStrategy::kBeforeComplete);
p.SetICDCheckInNodeId(NodeId{ 55 });
p.SetICDMonitoredSubject(static_cast<uint64_t>(99));
const uint8_t sym[16] = {};
p.SetICDSymmetricKey(ByteSpan{ sym });
p.SetICDClientType(app::Clusters::IcdManagement::ClientTypeEnum::kPermanent);
p.SetICDStayActiveDurationMsec(123456);
EXPECT_EQ(p.GetICDRegistrationStrategy(), ICDRegistrationStrategy::kBeforeComplete);
EXPECT_TRUE(p.GetICDCheckInNodeId().HasValue());
EXPECT_TRUE(p.GetICDMonitoredSubject().HasValue());
EXPECT_TRUE(p.GetICDSymmetricKey().HasValue());
EXPECT_TRUE(p.GetICDClientType().HasValue());
EXPECT_TRUE(p.GetICDStayActiveDurationMsec().HasValue());
p.ClearICDStayActiveDurationMsec();
EXPECT_FALSE(p.GetICDStayActiveDurationMsec().HasValue());
// Extra read paths
app::AttributePathParams attrs[2];
attrs[0] = app::AttributePathParams{ 1, 2, 3 };
attrs[1] = app::AttributePathParams{ 4, 5, 6 };
p.SetExtraReadPaths(Span<const app::AttributePathParams>(attrs, 2));
EXPECT_EQ(p.GetExtraReadPaths().size(), size_t{ 2 });
// Now clear all external buffer dependent values
p.ClearExternalBufferDependentValues();
EXPECT_FALSE(p.GetCSRNonce().HasValue());
EXPECT_FALSE(p.GetAttestationNonce().HasValue());
EXPECT_FALSE(p.GetWiFiCredentials().HasValue());
EXPECT_FALSE(p.GetCountryCode().HasValue());
EXPECT_FALSE(p.GetThreadOperationalDataset().HasValue());
EXPECT_FALSE(p.GetNOCChainGenerationParameters().HasValue());
EXPECT_FALSE(p.GetRootCert().HasValue());
EXPECT_FALSE(p.GetNoc().HasValue());
EXPECT_FALSE(p.GetIcac().HasValue());
EXPECT_FALSE(p.GetIpk().HasValue());
EXPECT_FALSE(p.GetAttestationElements().HasValue());
EXPECT_FALSE(p.GetAttestationSignature().HasValue());
EXPECT_FALSE(p.GetPAI().HasValue());
EXPECT_FALSE(p.GetDAC().HasValue());
EXPECT_FALSE(p.GetTimeZone().HasValue());
EXPECT_FALSE(p.GetDSTOffsets().HasValue());
EXPECT_FALSE(p.GetDefaultNTP().HasValue());
EXPECT_FALSE(p.GetICDSymmetricKey().HasValue());
EXPECT_EQ(p.GetExtraReadPaths().size(), size_t{ 0 });
}
// Small POD structs & CommissioningReport
TEST_F(CommissioningDelegateTest, PODStructConstructorsCover)
{
// WiFiCredentials
const uint8_t s[] = { 'X', 'Y' };
const uint8_t c[] = { 1, 2, 3 };
WiFiCredentials w(ByteSpan{ s }, ByteSpan{ c });
EXPECT_EQ(w.ssid.size(), sizeof(s));
EXPECT_EQ(w.credentials.size(), sizeof(c));
// RequestedCertificate / AttestationResponse / CSRResponse
const uint8_t cert[] = { 0xAA, 0xBB };
const uint8_t sig[] = { 0xCC };
RequestedCertificate rc(ByteSpan{ cert });
EXPECT_EQ(rc.certificate.size(), sizeof(cert));
AttestationResponse ar(ByteSpan{ cert }, ByteSpan{ sig });
EXPECT_EQ(ar.attestationElements.size(), sizeof(cert));
EXPECT_EQ(ar.signature.size(), sizeof(sig));
CSRResponse cr(ByteSpan{ cert }, ByteSpan{ sig });
EXPECT_EQ(cr.nocsrElements.size(), sizeof(cert));
EXPECT_EQ(cr.signature.size(), sizeof(sig));
// CommissioningReport default stage
CommissioningDelegate::CommissioningReport report;
EXPECT_EQ(report.stageCompleted, kError);
}
TEST_F(CommissioningDelegateTest, AttestationPayloadSettersStoreSpans)
{
CommissioningParameters p;
// Elements: arbitrary non-empty bytes (we only round-trip them here)
const uint8_t elems[] = { 0x01, 0x02, 0x03, 0x04, 0x05 };
// Signature: P-256 ECDSA is 64 bytes
std::array<uint8_t, 64> sig{ 0xA5, 0x5A };
p.SetAttestationElements(ByteSpan{ elems });
p.SetAttestationSignature(ByteSpan{ sig });
ASSERT_TRUE(p.GetAttestationElements().HasValue());
ASSERT_TRUE(p.GetAttestationSignature().HasValue());
const ByteSpan gotElems = p.GetAttestationElements().Value();
const ByteSpan gotSig = p.GetAttestationSignature().Value();
EXPECT_EQ(gotElems.size(), sizeof(elems));
EXPECT_EQ(gotSig.size(), sizeof(sig));
EXPECT_TRUE(gotElems.data_equal(ByteSpan{ elems }));
EXPECT_TRUE(gotSig.data_equal(ByteSpan{ sig }));
}
TEST_F(CommissioningDelegateTest, CertSettersStoreSpans)
{
CommissioningParameters p;
const uint8_t pai[256] = { 0x11, 0x22, 0x33 };
const uint8_t dac[256] = { 0x44, 0x55 };
p.SetPAI(ByteSpan{ pai });
p.SetDAC(ByteSpan{ dac });
ASSERT_TRUE(p.GetPAI().HasValue());
ASSERT_TRUE(p.GetDAC().HasValue());
const ByteSpan gotPAI = p.GetPAI().Value();
const ByteSpan gotDAC = p.GetDAC().Value();
EXPECT_EQ(gotPAI.size(), sizeof(pai));
EXPECT_EQ(gotDAC.size(), sizeof(dac));
EXPECT_TRUE(gotPAI.data_equal(ByteSpan{ pai }));
EXPECT_TRUE(gotDAC.data_equal(ByteSpan{ dac }));
}
TEST_F(CommissioningDelegateTest, RemoteNodeIdSetterStoresValue)
{
CommissioningParameters p;
const NodeId id = 0x1111222233334444ULL;
p.SetRemoteNodeId(id);
ASSERT_TRUE(p.GetRemoteNodeId().HasValue());
EXPECT_EQ(p.GetRemoteNodeId().Value(), id);
}
TEST_F(CommissioningDelegateTest, CompletionStatus_DefaultAndRoundTrip)
{
CommissioningParameters p;
// Defaults
const CompletionStatus & def = p.GetCompletionStatus();
EXPECT_EQ(def.err, CHIP_NO_ERROR);
EXPECT_FALSE(def.failedStage.HasValue());
EXPECT_FALSE(def.attestationResult.HasValue());
EXPECT_FALSE(def.commissioningError.HasValue());
EXPECT_FALSE(def.networkCommissioningStatus.HasValue());
// Populate and set
CompletionStatus cs;
cs.err = CHIP_ERROR_INTERNAL;
cs.failedStage = MakeOptional(CommissioningStage::kAttestationVerification);
cs.attestationResult = MakeOptional(Credentials::AttestationVerificationResult::kPaiAuthorityNotFound);
cs.commissioningError = MakeOptional(app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kValueOutsideRange);
cs.networkCommissioningStatus =
MakeOptional(app::Clusters::NetworkCommissioning::NetworkCommissioningStatusEnum::kBoundsExceeded);
p.SetCompletionStatus(cs);
const CompletionStatus & got = p.GetCompletionStatus();
EXPECT_EQ(got.err, CHIP_ERROR_INTERNAL);
ASSERT_TRUE(got.failedStage.HasValue());
EXPECT_EQ(got.failedStage.Value(), CommissioningStage::kAttestationVerification);
ASSERT_TRUE(got.attestationResult.HasValue());
EXPECT_EQ(got.attestationResult.Value(), Credentials::AttestationVerificationResult::kPaiAuthorityNotFound);
ASSERT_TRUE(got.commissioningError.HasValue());
EXPECT_EQ(got.commissioningError.Value(), app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kValueOutsideRange);
ASSERT_TRUE(got.networkCommissioningStatus.HasValue());
EXPECT_EQ(got.networkCommissioningStatus.Value(),
app::Clusters::NetworkCommissioning::NetworkCommissioningStatusEnum::kBoundsExceeded);
}
} // namespace