blob: 8f4a9019585e2ffb315b6eb954c20f07216b7eb9 [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 <pw_unit_test/framework.h>
#include <app/clusters/av-analysis-server/DefaultAvAnalysisWebRTCClient.h>
#include <app/clusters/webrtc-transport-requestor-server/WebRTCTransportRequestorCluster.h>
#include <clusters/Descriptor/Attributes.h>
#include <clusters/Descriptor/Ids.h>
#include <clusters/WebRTCTransportProvider/Ids.h>
#include <lib/core/TLV.h>
namespace {
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace Protocols::InteractionModel;
constexpr EndpointId kProviderEndpoint = 2;
constexpr EndpointId kRequestorEndpoint = 7;
const ScopedNodeId kCameraNode(0x1234, 1);
constexpr uint8_t kMaxSessions = 4;
// Intercepts the network boundary: records session requests instead of establishing CASE sessions,
// and captures the command payloads instead of sending them. The camera's behavior is simulated
// by invoking the interaction callbacks with the sender each send recorded as the request's.
class InterceptingWebRTCClient : public DefaultAvAnalysisWebRTCClient
{
public:
using DefaultAvAnalysisWebRTCClient::CurrentRequest;
using DefaultAvAnalysisWebRTCClient::Request;
// The interaction callbacks, so the tests can play the camera's and the application's side
using DefaultAvAnalysisWebRTCClient::OnAttributeData;
using DefaultAvAnalysisWebRTCClient::OnDone;
using DefaultAvAnalysisWebRTCClient::OnError;
using DefaultAvAnalysisWebRTCClient::OnOfferReady;
using DefaultAvAnalysisWebRTCClient::OnResponse;
// Drives the request into the provider-check phase, as OnDeviceConnected would after CASE
void EnterProviderCheck() { CurrentRequest().Advance(Request::Phase::kCheckingProvider); }
// The CASE outcomes, so the tests can play the CASESessionManager's side. A session cannot be
// fabricated here, so the connected case is driven through the continuation OnDeviceConnected
// runs once it has held one.
using DefaultAvAnalysisWebRTCClient::ContinueWithSession;
using DefaultAvAnalysisWebRTCClient::OnDeviceConnectionFailure;
// The sender every intercepted send records as the request's; never sends, an identity only
CommandSender * Sender() { return &mSender; }
// What every intercepted send returns; a failure returns before anything is recorded as sent
CHIP_ERROR mSendResult = CHIP_NO_ERROR;
// What an intercepted send returns after recording itself, as production does when
// AddRequestData or SendCommandRequest fails with the phase already advanced. Exercises the
// rollback that lets the client take another request.
CHIP_ERROR mSendResultAfterRecording = CHIP_NO_ERROR;
int mConnectRequests = 0;
ScopedNodeId mLastPeer;
// The ProvideOffer payload the client would have sent
int mSendAttempts = 0;
bool mSentSessionIdWasNull = false;
std::string mSentSdp;
Globals::StreamUsageEnum mSentUsage = Globals::StreamUsageEnum::kUnknownEnumValue;
EndpointId mSentOriginatingEndpoint = kInvalidEndpointId;
std::vector<uint16_t> mSentVideoStreams;
bool mSentDeprecatedStreamIdAbsent = false;
bool mSentAudioAbsent = false;
bool mSentIceAbsent = false;
// The EndSession payload the client would have sent
int mEndSendAttempts = 0;
uint16_t mSentEndSessionId = 0;
Globals::WebRTCEndReasonEnum mSentEndReason = Globals::WebRTCEndReasonEnum::kUnknownEnumValue;
// Drives a pending EndSession request past CASE, as OnDeviceConnected would
CHIP_ERROR SendPendingEndSession() { return SendEndSession(); }
// The ProvideICECandidates payload the client would have sent
int mIceSendAttempts = 0;
uint16_t mSentIceSessionId = 0;
std::vector<std::string> mSentCandidates;
std::vector<std::optional<std::string>> mSentMids;
std::vector<DataModel::Nullable<uint16_t>> mSentMLineIndexes;
// Drives a pending ProvideICECandidates request past CASE, as OnDeviceConnected would
CHIP_ERROR SendPendingICECandidates() { return SendProvideICECandidates(); }
protected:
void EstablishSession(const ScopedNodeId & aCameraNode) override
{
mConnectRequests++;
mLastPeer = aCameraNode;
}
CHIP_ERROR SendProvideOffer() override
{
mSendAttempts++;
ReturnErrorOnFailure(mSendResult);
uint16_t videoStream = 0;
WebRTCTransportProvider::Commands::ProvideOffer::Type request;
ReturnErrorOnFailure(BuildProvideOffer(request, videoStream));
mSentSessionIdWasNull = request.webRTCSessionID.IsNull();
mSentSdp = std::string(request.sdp.data(), request.sdp.size());
mSentUsage = request.streamUsage;
mSentOriginatingEndpoint = request.originatingEndpointID;
mSentVideoStreams.clear();
if (request.videoStreams.HasValue())
{
for (uint16_t stream : request.videoStreams.Value())
{
mSentVideoStreams.push_back(stream);
}
}
mSentDeprecatedStreamIdAbsent = !request.videoStreamID.HasValue();
mSentAudioAbsent = !request.audioStreamID.HasValue() && !request.audioStreams.HasValue();
mSentIceAbsent = !request.ICEServers.HasValue() && !request.ICETransportPolicy.HasValue();
return MarkSent();
}
CHIP_ERROR SendEndSession() override
{
mEndSendAttempts++;
ReturnErrorOnFailure(mSendResult);
WebRTCTransportProvider::Commands::EndSession::Type request;
ReturnErrorOnFailure(BuildEndSession(request));
mSentEndSessionId = request.webRTCSessionID;
mSentEndReason = request.reason;
return MarkSent();
}
CHIP_ERROR SendProvideICECandidates() override
{
mIceSendAttempts++;
ReturnErrorOnFailure(mSendResult);
std::vector<Globals::Structs::ICECandidateStruct::Type> storage;
WebRTCTransportProvider::Commands::ProvideICECandidates::Type request;
ReturnErrorOnFailure(BuildProvideICECandidates(request, storage));
mSentIceSessionId = request.webRTCSessionID;
mSentCandidates.clear();
mSentMids.clear();
mSentMLineIndexes.clear();
for (const auto & candidate : request.ICECandidates)
{
mSentCandidates.emplace_back(candidate.candidate.data(), candidate.candidate.size());
mSentMids.emplace_back(
candidate.SDPMid.IsNull()
? std::nullopt
: std::optional<std::string>(std::string(candidate.SDPMid.Value().data(), candidate.SDPMid.Value().size())));
mSentMLineIndexes.push_back(candidate.SDPMLineIndex);
}
return MarkSent();
}
private:
// As InvokeOnHeldSession records a send: the camera's answer arrives through the sender callbacks
CHIP_ERROR MarkSent()
{
CurrentRequest().SetInvokedSender(&mSender);
CurrentRequest().Advance(Request::Phase::kInvoking);
// Left recorded and advanced deliberately: undoing it is the caller's job, which is what
// these failures exercise
return mSendResultAfterRecording;
}
CommandSender mSender{ nullptr, nullptr };
};
constexpr uint16_t kVideoStreamId = 42;
class RecordingCallback : public AvAnalysisWebRTCClient::Callback
{
public:
int mInitiatedCount = 0;
int mActiveCount = 0;
int mFailedCount = 0;
int mEndedCount = 0;
// A status no completion delivers, so a Success can only come from one
Status mLastStatus = Status::InvalidAction;
uint16_t mLastSession = 0;
bool mLastOfferSent = false;
void OnSessionInitiated(Status aStatus, uint16_t aWebRTCSessionId, bool aOfferSent) override
{
mInitiatedCount++;
mLastOfferSent = aOfferSent;
mLastStatus = aStatus;
mLastSession = aWebRTCSessionId;
}
void OnSessionActive(const ScopedNodeId & aCameraNode, uint16_t aWebRTCSessionId) override
{
mActiveCount++;
mLastCamera = aCameraNode;
mLastSession = aWebRTCSessionId;
}
void OnSessionFailed(const ScopedNodeId & aCameraNode, uint16_t aWebRTCSessionId) override
{
mFailedCount++;
mLastCamera = aCameraNode;
mLastSession = aWebRTCSessionId;
}
ScopedNodeId mLastCamera;
void OnSessionEnded(Status aStatus, uint16_t aWebRTCSessionId) override
{
mEndedCount++;
mLastStatus = aStatus;
mLastSession = aWebRTCSessionId;
}
};
// Hands back a canned SDP synchronously; records the session lifecycle notifications
class FakePeerDelegate : public AvAnalysisWebRTCPeerDelegate
{
public:
int mOffersRequested = 0;
int mOffersAbandoned = 0;
int mSessionsAssigned = 0;
int mSessionsClosed = 0;
uint16_t mLastAssigned = 0;
uint16_t mLastClosed = 0;
CHIP_ERROR CreateOffer(OfferCallback & aCallback) override
{
mOffersRequested++;
mLastOfferCallback = &aCallback;
if (mDeliverOfferSynchronously)
{
aCallback.OnOfferReady(CHIP_NO_ERROR, "v=0 synchronous offer"_span);
}
return mCreateOfferResult;
}
CHIP_ERROR mCreateOfferResult = CHIP_NO_ERROR;
bool mDeliverOfferSynchronously = false;
void OnSessionAssigned(const ScopedNodeId & aCameraNode, uint16_t aWebRTCSessionId) override
{
mSessionsAssigned++;
mLastAssigned = aWebRTCSessionId;
// The contract: the request that produced the offer is over
if (mClient != nullptr)
{
mRequestInFlightAtAssign = mClient->CurrentRequest().InFlight();
if (mSendCandidatesOnAssign)
{
Globals::Structs::ICECandidateStruct::Type candidate;
candidate.candidate = "candidate:1 1 UDP 2122252543 192.168.1.10 5000 typ host"_span;
mSendResultAtAssign = mClient->SendICECandidates(
aCameraNode, aWebRTCSessionId, Span<const Globals::Structs::ICECandidateStruct::Type>(&candidate, 1));
}
}
}
InterceptingWebRTCClient * mClient = nullptr;
bool mRequestInFlightAtAssign = true;
bool mSendCandidatesOnAssign = false;
CHIP_ERROR mSendResultAtAssign = CHIP_ERROR_INTERNAL;
void OnOfferAbandoned() override { mOffersAbandoned++; }
void OnSessionClosed(const ScopedNodeId & aCameraNode, uint16_t aWebRTCSessionId) override
{
mSessionsClosed++;
mLastClosedCamera = aCameraNode;
mLastClosed = aWebRTCSessionId;
// An application whose peer connection reports its own close synchronously
if (mReportFailureOnClose && mClient != nullptr)
{
mClient->NotifyFailed(aCameraNode, aWebRTCSessionId);
}
}
ScopedNodeId mLastClosedCamera;
bool mReportFailureOnClose = false;
OfferCallback * mLastOfferCallback = nullptr;
};
// The requestor cluster the client records sessions into
class StubRequestorDelegate : public WebRTCTransportRequestor::Delegate
{
public:
CHIP_ERROR HandleOffer(const WebRTCTransportRequestor::WebRTCSessionStruct &, const OfferArgs &) override
{
return CHIP_NO_ERROR;
}
CHIP_ERROR HandleAnswer(const WebRTCTransportRequestor::WebRTCSessionStruct &, const std::string &) override
{
return CHIP_NO_ERROR;
}
CHIP_ERROR HandleICECandidates(const WebRTCTransportRequestor::WebRTCSessionStruct &,
const std::vector<WebRTCTransportRequestor::ICECandidateStruct> &) override
{
return CHIP_NO_ERROR;
}
CHIP_ERROR HandleEnd(const WebRTCTransportRequestor::WebRTCSessionStruct &,
WebRTCTransportRequestor::WebRTCEndReasonEnum) override
{
return CHIP_NO_ERROR;
}
};
ConcreteDataAttributePath ServerListPath(EndpointId aEndpoint)
{
return ConcreteDataAttributePath(aEndpoint, Descriptor::Id, Descriptor::Attributes::ServerList::Id);
}
// Feeds a cluster-id list report for aPath with aStatus into the client, as the read would deliver it
void FeedClusterList(InterceptingWebRTCClient & aClient, const ConcreteDataAttributePath & aPath, Span<const ClusterId> aClusters,
const StatusIB & aStatus)
{
uint8_t buffer[64];
TLV::TLVWriter writer;
writer.Init(buffer, sizeof(buffer));
TLV::TLVType outer;
EXPECT_EQ(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, outer), CHIP_NO_ERROR);
for (ClusterId cluster : aClusters)
{
EXPECT_EQ(writer.Put(TLV::AnonymousTag(), cluster), CHIP_NO_ERROR);
}
EXPECT_EQ(writer.EndContainer(outer), CHIP_NO_ERROR);
TLV::TLVReader reader;
reader.Init(buffer, writer.GetLengthWritten());
const bool positioned = (reader.Next() == CHIP_NO_ERROR);
EXPECT_TRUE(positioned);
VerifyOrReturn(positioned);
aClient.OnAttributeData(aPath, &reader, aStatus);
}
// A successful ServerList report for aEndpoint
void FeedServerList(InterceptingWebRTCClient & aClient, EndpointId aEndpoint, Span<const ClusterId> aClusters)
{
FeedClusterList(aClient, ServerListPath(aEndpoint), aClusters, StatusIB());
}
struct TestDefaultAvAnalysisWebRTCClient : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); }
void SetUp() override
{
ASSERT_EQ(mClient.Init(&mCASESessionManager, &mPeerDelegate, &mRequestorCluster, kMaxSessions), CHIP_NO_ERROR);
mPeerDelegate.mClient = &mClient;
}
// Drives a request up to the application's turn: CASE up, provider present, offer asked for
void DriveToOffer() { DriveToOffer(mCallback); }
void DriveToOffer(RecordingCallback & aCallback)
{
ASSERT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, kVideoStreamId, aCallback), CHIP_NO_ERROR);
mClient.EnterProviderCheck();
const ClusterId kProviderList[] = { WebRTCTransportProvider::Id };
FeedServerList(mClient, kProviderEndpoint, Span<const ClusterId>(kProviderList));
mClient.OnDone(static_cast<ReadClient *>(nullptr));
}
// Feeds the camera's ProvideOfferResponse{sessionId, videoStreamId} into the client
void FeedOfferResponse(uint16_t aWebRTCSessionId) { FeedOfferResponse(aWebRTCSessionId, kVideoStreamId); }
void FeedOfferResponse(uint16_t aWebRTCSessionId, std::optional<uint16_t> aVideoStreamId)
{
FeedOfferResponseFrom(mClient.Sender(), aWebRTCSessionId, aVideoStreamId);
}
// The response as delivered by aSender, which need not be the request's own
void FeedOfferResponseFrom(CommandSender * aSender, uint16_t aWebRTCSessionId, std::optional<uint16_t> aVideoStreamId)
{
using Fields = WebRTCTransportProvider::Commands::ProvideOfferResponse::Fields;
uint8_t buffer[64];
TLV::TLVWriter writer;
writer.Init(buffer);
TLV::TLVType containerType;
EXPECT_EQ(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, containerType), CHIP_NO_ERROR);
EXPECT_EQ(writer.Put(TLV::ContextTag(Fields::kWebRTCSessionID), aWebRTCSessionId), CHIP_NO_ERROR);
if (aVideoStreamId.has_value())
{
EXPECT_EQ(writer.Put(TLV::ContextTag(Fields::kVideoStreamID), *aVideoStreamId), CHIP_NO_ERROR);
}
EXPECT_EQ(writer.EndContainer(containerType), CHIP_NO_ERROR);
TLV::TLVReader reader;
reader.Init(buffer, writer.GetLengthWritten());
const bool positioned = (reader.Next() == CHIP_NO_ERROR);
EXPECT_TRUE(positioned);
VerifyOrReturn(positioned);
ConcreteCommandPath responsePath(kProviderEndpoint, WebRTCTransportProvider::Id,
WebRTCTransportProvider::Commands::ProvideOfferResponse::Id);
mClient.OnResponse(aSender, responsePath, StatusIB(), &reader);
}
// The full successful offer exchange, ending with the session tracked under aWebRTCSessionId
void EstablishSessionWithId(uint16_t aWebRTCSessionId) { EstablishSessionWithId(aWebRTCSessionId, mCallback); }
void EstablishSessionWithId(uint16_t aWebRTCSessionId, RecordingCallback & aCallback)
{
EstablishSessionWithId(aWebRTCSessionId, aCallback, kCameraNode);
}
// The full exchange toward a specific camera, for sessions of several cameras side by side
void EstablishSessionWithId(uint16_t aWebRTCSessionId, RecordingCallback & aCallback, const ScopedNodeId & aCameraNode)
{
ASSERT_EQ(mClient.RequestSession(aCameraNode, kProviderEndpoint, kVideoStreamId, aCallback), CHIP_NO_ERROR);
mClient.EnterProviderCheck();
const ClusterId kProviderList[] = { WebRTCTransportProvider::Id };
FeedServerList(mClient, kProviderEndpoint, Span<const ClusterId>(kProviderList));
mClient.OnDone(static_cast<ReadClient *>(nullptr));
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
FeedOfferResponse(aWebRTCSessionId);
mClient.OnDone(mClient.Sender());
}
// Requests the end of a tracked session and sends the command, up to the camera's answer
void DriveToEndSessionSent(uint16_t aWebRTCSessionId)
{
ASSERT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, aWebRTCSessionId, mCallback), CHIP_NO_ERROR);
ASSERT_EQ(mClient.SendPendingEndSession(), CHIP_NO_ERROR);
}
// Feeds the camera's status-only answer to EndSession into the client
void FeedEndSessionStatus(Status aStatus)
{
ConcreteCommandPath responsePath(kProviderEndpoint, WebRTCTransportProvider::Id,
WebRTCTransportProvider::Commands::EndSession::Id);
mClient.OnResponse(mClient.Sender(), responsePath, StatusIB(aStatus), nullptr);
}
// Two candidates as the application would hand them over: one with a mid, one without
static std::vector<Globals::Structs::ICECandidateStruct::Type> TwoCandidates()
{
std::vector<Globals::Structs::ICECandidateStruct::Type> candidates(2);
candidates[0].candidate = "candidate:1 1 UDP 2122252543 192.168.1.10 5000 typ host"_span;
candidates[0].SDPMid.SetNonNull("video"_span);
candidates[0].SDPMLineIndex.SetNonNull(static_cast<uint16_t>(0));
candidates[1].candidate = "candidate:2 1 UDP 1686052607 203.0.113.5 5000 typ srflx"_span;
candidates[1].SDPMid.SetNull();
candidates[1].SDPMLineIndex.SetNull();
return candidates;
}
// Requests a candidate send for a tracked session and sends the command, up to the camera's answer
void DriveToICECandidatesSent(uint16_t aWebRTCSessionId)
{
auto candidates = TwoCandidates();
ASSERT_EQ(
mClient.SendICECandidates(kCameraNode, aWebRTCSessionId,
Span<const Globals::Structs::ICECandidateStruct::Type>(candidates.data(), candidates.size())),
CHIP_NO_ERROR);
ASSERT_EQ(mClient.SendPendingICECandidates(), CHIP_NO_ERROR);
}
CASESessionManager mCASESessionManager;
RecordingCallback mCallback;
FakePeerDelegate mPeerDelegate;
StubRequestorDelegate mRequestorDelegate;
// Not endpoint 1: the offer's OriginatingEndpointID must come from this registration and not
// from a plausible default
WebRTCTransportRequestor::WebRTCTransportRequestorCluster mRequestorCluster{ kRequestorEndpoint, mRequestorDelegate };
// Declared last: the client's destructor cancels through the delegate and the requestor
// cluster, so they must still be alive. InterceptingWebRTCClient overrides EstablishSession.
InterceptingWebRTCClient mClient;
};
TEST_F(TestDefaultAvAnalysisWebRTCClient, InitArgumentValidation)
{
DefaultAvAnalysisWebRTCClient client;
EXPECT_EQ(client.Init(nullptr, &mPeerDelegate, &mRequestorCluster, kMaxSessions), CHIP_ERROR_INVALID_ARGUMENT);
EXPECT_EQ(client.Init(&mCASESessionManager, nullptr, &mRequestorCluster, kMaxSessions), CHIP_ERROR_INVALID_ARGUMENT);
EXPECT_EQ(client.Init(&mCASESessionManager, &mPeerDelegate, nullptr, kMaxSessions), CHIP_ERROR_INVALID_ARGUMENT);
EXPECT_EQ(client.Init(&mCASESessionManager, &mPeerDelegate, &mRequestorCluster, 0), CHIP_ERROR_INVALID_ARGUMENT);
EXPECT_EQ(client.Init(&mCASESessionManager, &mPeerDelegate, &mRequestorCluster, kMaxSessions), CHIP_NO_ERROR);
EXPECT_EQ(client.Init(&mCASESessionManager, &mPeerDelegate, &mRequestorCluster, kMaxSessions), CHIP_ERROR_INCORRECT_STATE);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, RequestStartsSessionEstablishment)
{
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
EXPECT_EQ(mClient.mConnectRequests, 1);
EXPECT_EQ(mClient.mLastPeer, kCameraNode);
EXPECT_EQ(mCallback.mInitiatedCount, 0); // Nothing completed yet
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, SecondRequestWhilePendingIsBusy)
{
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_ERROR_BUSY);
EXPECT_EQ(mClient.mConnectRequests, 1);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ACameraThatCannotBeReachedFailsTheRequest)
{
ASSERT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, kVideoStreamId, mCallback), CHIP_NO_ERROR);
mClient.OnDeviceConnectionFailure(&mClient, kCameraNode, CHIP_ERROR_TIMEOUT);
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
// The request is over rather than stuck: the client takes the next one
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, kVideoStreamId, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ACameraThatCannotBeReachedFailsAnEndSession)
{
EstablishSessionWithId(55);
ASSERT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_NO_ERROR);
mClient.OnDeviceConnectionFailure(&mClient, kCameraNode, CHIP_ERROR_TIMEOUT);
EXPECT_EQ(mCallback.mEndedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
// This node is done with the session whatever became of the EndSession
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AConnectionFailureOutsideTheConnectingPhaseIsIgnored)
{
DriveToOffer(); // the request is awaiting the application's offer, no longer connecting
mClient.OnDeviceConnectionFailure(&mClient, kCameraNode, CHIP_ERROR_TIMEOUT);
EXPECT_EQ(mCallback.mInitiatedCount, 0);
EXPECT_TRUE(mClient.CurrentRequest().InPhase(InterceptingWebRTCClient::Request::Phase::kCreatingOffer));
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AHeldSessionSendsTheCommandTheRequestIsFor)
{
// An offer request checks the provider before anything is sent
ASSERT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, kVideoStreamId, mCallback), CHIP_NO_ERROR);
mClient.ContinueWithSession();
EXPECT_EQ(mClient.mSendAttempts, 0);
EXPECT_EQ(mClient.mEndSendAttempts, 0);
EXPECT_EQ(mClient.mIceSendAttempts, 0);
// No session is really held, so the read cannot start and the request fails
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
// An EndSession request sends EndSession
EstablishSessionWithId(55);
const int offersBefore = mClient.mSendAttempts;
ASSERT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_NO_ERROR);
mClient.ContinueWithSession();
EXPECT_EQ(mClient.mEndSendAttempts, 1);
EXPECT_EQ(mClient.mSentEndSessionId, 55);
EXPECT_EQ(mClient.mIceSendAttempts, 0);
EXPECT_EQ(mClient.mSendAttempts, offersBefore);
FeedEndSessionStatus(Status::Success);
mClient.OnDone(mClient.Sender());
// A candidate request sends ProvideICECandidates
EstablishSessionWithId(56);
auto candidates = TwoCandidates();
ASSERT_EQ(mClient.SendICECandidates(
kCameraNode, 56, Span<const Globals::Structs::ICECandidateStruct::Type>(candidates.data(), candidates.size())),
CHIP_NO_ERROR);
mClient.ContinueWithSession();
EXPECT_EQ(mClient.mIceSendAttempts, 1);
EXPECT_EQ(mClient.mSentIceSessionId, 56);
EXPECT_EQ(mClient.mEndSendAttempts, 1);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnEndSessionTheClientCannotSendFailsTheRequest)
{
EstablishSessionWithId(55);
ASSERT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_NO_ERROR);
mClient.mSendResult = CHIP_ERROR_INTERNAL;
mClient.ContinueWithSession();
EXPECT_EQ(mCallback.mEndedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
// Completed: the client accepts a new request again
mClient.mSendResult = CHIP_NO_ERROR;
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, kVideoStreamId, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, CandidatesTheClientCannotSendKeepTheSession)
{
EstablishSessionWithId(55);
auto candidates = TwoCandidates();
ASSERT_EQ(mClient.SendICECandidates(
kCameraNode, 55, Span<const Globals::Structs::ICECandidateStruct::Type>(candidates.data(), candidates.size())),
CHIP_NO_ERROR);
mClient.mSendResult = CHIP_ERROR_INTERNAL;
mClient.ContinueWithSession();
// Nobody awaits a candidate send, and the session it was about survives
EXPECT_EQ(mCallback.mEndedCount, 0);
EXPECT_EQ(mCallback.mFailedCount, 0);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 1u);
mClient.mSendResult = CHIP_NO_ERROR;
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, EndingAnUntrackedSessionIsRejected)
{
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_ERROR_INVALID_ARGUMENT);
EXPECT_EQ(mClient.mConnectRequests, 0);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, EndingASessionAtAnotherEndpointIsRejected)
{
EstablishSessionWithId(55);
const int connectsBefore = mClient.mConnectRequests;
// The session was initiated on kProviderEndpoint; EndSession must go there
EXPECT_EQ(mClient.EndSession(kCameraNode, static_cast<EndpointId>(kProviderEndpoint + 1), 55, mCallback),
CHIP_ERROR_INVALID_ARGUMENT);
EXPECT_EQ(mClient.mConnectRequests, connectsBefore);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 1u);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, MissingProviderClusterIsNotFound)
{
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
mClient.EnterProviderCheck();
// The endpoint's ServerList lacks WebRTCTransportProvider
const ClusterId kOtherClusters[] = { Descriptor::Id, 0x0557 };
FeedServerList(mClient, kProviderEndpoint, Span<const ClusterId>(kOtherClusters));
mClient.OnDone(static_cast<ReadClient *>(nullptr));
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::NotFound);
// Completed: the client accepts a new request again
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ReportsForOtherPathsDoNotSatisfyTheProviderCheck)
{
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
mClient.EnterProviderCheck();
// The provider exists, but on a different endpoint than the command named
const ClusterId kProviderList[] = { WebRTCTransportProvider::Id };
FeedServerList(mClient, static_cast<EndpointId>(kProviderEndpoint + 1), Span<const ClusterId>(kProviderList));
mClient.OnDone(static_cast<ReadClient *>(nullptr));
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::NotFound);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnUndecodableServerListFailsTheRequestRatherThanNotFound)
{
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
mClient.EnterProviderCheck();
// A report for the right path whose payload is not a list
uint8_t buffer[8];
TLV::TLVWriter writer;
writer.Init(buffer, sizeof(buffer));
ASSERT_EQ(writer.Put(TLV::AnonymousTag(), static_cast<uint32_t>(7)), CHIP_NO_ERROR);
TLV::TLVReader reader;
reader.Init(buffer, writer.GetLengthWritten());
ASSERT_EQ(reader.Next(), CHIP_NO_ERROR);
mClient.OnAttributeData(ServerListPath(kProviderEndpoint), &reader, StatusIB());
mClient.OnDone(static_cast<ReadClient *>(nullptr));
// Nothing is known about the endpoint, so the cluster is not reported absent
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AServerListThatCannotBeIteratedFailsTheRequestRatherThanNotFound)
{
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
mClient.EnterProviderCheck();
// A list whose second element is not a cluster id: iteration stops before the list is known
uint8_t buffer[32];
TLV::TLVWriter writer;
writer.Init(buffer, sizeof(buffer));
TLV::TLVType outer;
ASSERT_EQ(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, outer), CHIP_NO_ERROR);
ASSERT_EQ(writer.Put(TLV::AnonymousTag(), Descriptor::Id), CHIP_NO_ERROR);
ASSERT_EQ(writer.PutString(TLV::AnonymousTag(), "x"), CHIP_NO_ERROR);
ASSERT_EQ(writer.EndContainer(outer), CHIP_NO_ERROR);
TLV::TLVReader reader;
reader.Init(buffer, writer.GetLengthWritten());
ASSERT_EQ(reader.Next(), CHIP_NO_ERROR);
mClient.OnAttributeData(ServerListPath(kProviderEndpoint), &reader, StatusIB());
mClient.OnDone(static_cast<ReadClient *>(nullptr));
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnErrorStatusForTheServerListIsNotFound)
{
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
mClient.EnterProviderCheck();
// The camera has no such endpoint: whatever the report carries, its status says the list is not to be had
const ClusterId kProviderList[] = { WebRTCTransportProvider::Id };
FeedClusterList(mClient, ServerListPath(kProviderEndpoint), Span<const ClusterId>(kProviderList),
StatusIB(Status::UnsupportedEndpoint));
mClient.OnAttributeData(ServerListPath(kProviderEndpoint), nullptr, StatusIB(Status::UnsupportedEndpoint));
mClient.OnDone(static_cast<ReadClient *>(nullptr));
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::NotFound);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AServerListReportOutsideTheProviderCheckIsIgnored)
{
// CASE is still being established: a report cannot be this request's
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
const ClusterId kProviderList[] = { WebRTCTransportProvider::Id };
FeedServerList(mClient, kProviderEndpoint, Span<const ClusterId>(kProviderList));
// The check that follows starts with nothing known
mClient.EnterProviderCheck();
mClient.OnDone(static_cast<ReadClient *>(nullptr));
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::NotFound);
EXPECT_EQ(mPeerDelegate.mOffersRequested, 0);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AProviderListReportedUnderAnotherPathIsNotTheProviderCheck)
{
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
mClient.EnterProviderCheck();
// The provider is listed, but under the ClientList attribute of the right endpoint
const ClusterId kProviderList[] = { WebRTCTransportProvider::Id };
FeedClusterList(mClient, ConcreteDataAttributePath(kProviderEndpoint, Descriptor::Id, Descriptor::Attributes::ClientList::Id),
Span<const ClusterId>(kProviderList), StatusIB());
mClient.OnDone(static_cast<ReadClient *>(nullptr));
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::NotFound);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ReadCallbacksOutsideTheProviderCheckAreIgnored)
{
// An EndSession is in flight: no read of ours is
EstablishSessionWithId(55);
DriveToEndSessionSent(55);
mClient.OnError(CHIP_ERROR_ACCESS_DENIED);
mClient.OnDone(static_cast<ReadClient *>(nullptr));
// The request is untouched, still awaiting the camera's answer
EXPECT_EQ(mClient.CurrentRequest().GetPhase(), InterceptingWebRTCClient::Request::Phase::kInvoking);
EXPECT_EQ(mCallback.mEndedCount, 0);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 1u);
FeedEndSessionStatus(Status::Success);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mEndedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AFailedServerListReadFailsTheRequestRatherThanNotFound)
{
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
mClient.EnterProviderCheck();
// The read itself fails (for instance the camera denies access to the Descriptor cluster)
mClient.OnError(CHIP_ERROR_ACCESS_DENIED);
mClient.OnDone(static_cast<ReadClient *>(nullptr));
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, CancelSilentlyAbandonsTheRequest)
{
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
mClient.Cancel();
EXPECT_EQ(mCallback.mInitiatedCount, 0);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 0); // No offer was asked for yet
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, 42, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ProviderFoundRequestsAnOfferFromTheApplication)
{
DriveToOffer();
EXPECT_EQ(mPeerDelegate.mOffersRequested, 1);
EXPECT_EQ(mClient.mSendAttempts, 0); // No SDP yet, nothing to send
EXPECT_EQ(mCallback.mInitiatedCount, 0); // Awaiting the application
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, OfferHookRefusalFailsTheRequest)
{
mPeerDelegate.mCreateOfferResult = CHIP_ERROR_INTERNAL;
DriveToOffer();
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 1); // Whatever the refusal left behind is released
// Completed: the client accepts a new request again
mPeerDelegate.mCreateOfferResult = CHIP_NO_ERROR;
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, kVideoStreamId, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnOfferDeliveredBeforeCreateOfferReturnsAnErrorStands)
{
// The application hands the offer over from within CreateOffer, then returns an error anyway
mPeerDelegate.mDeliverOfferSynchronously = true;
mPeerDelegate.mCreateOfferResult = CHIP_ERROR_INTERNAL;
DriveToOffer();
// The offer is on the wire: the late error must not fail the request behind it
EXPECT_EQ(mClient.mSendAttempts, 1);
EXPECT_EQ(mClient.CurrentRequest().GetPhase(), InterceptingWebRTCClient::Request::Phase::kInvoking);
EXPECT_EQ(mCallback.mInitiatedCount, 0);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 0);
// The camera's answer concludes it
FeedOfferResponse(55);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
EXPECT_EQ(mCallback.mLastSession, 55);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ApplicationOfferFailureFailsTheRequest)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_ERROR_INTERNAL, CharSpan());
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mClient.mSendAttempts, 0);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, StaleOfferDeliveryIsIgnored)
{
mClient.OnOfferReady(CHIP_NO_ERROR, "v=0 stale offer"_span);
EXPECT_EQ(mCallback.mInitiatedCount, 0);
EXPECT_EQ(mClient.mSendAttempts, 0);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, OfferedSessionIsEstablishedAndRegistered)
{
EstablishSessionWithId(55);
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
EXPECT_EQ(mCallback.mLastSession, 55);
// The application was told which id its pending peer connection now has
EXPECT_EQ(mPeerDelegate.mSessionsAssigned, 1);
EXPECT_EQ(mPeerDelegate.mLastAssigned, 55);
// The requestor cluster validates the camera's inbound commands against this record
auto sessions = mRequestorCluster.GetCurrentSessions();
ASSERT_EQ(sessions.size(), 1u);
EXPECT_EQ(sessions[0].id, 55);
EXPECT_EQ(sessions[0].peerNodeID, kCameraNode.GetNodeId());
EXPECT_EQ(sessions[0].fabricIndex, kCameraNode.GetFabricIndex());
EXPECT_EQ(sessions[0].peerEndpointID, kProviderEndpoint);
EXPECT_EQ(sessions[0].streamUsage, Globals::StreamUsageEnum::kAnalysis);
ASSERT_FALSE(sessions[0].videoStreamID.IsNull());
EXPECT_EQ(sessions[0].videoStreamID.Value(), kVideoStreamId);
// Tracked: ending this session is now a legitimate request
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, SessionAssignmentFollowsTheCompletedRequest)
{
// The application flushes its candidates the moment it learns the session id
mPeerDelegate.mSendCandidatesOnAssign = true;
EstablishSessionWithId(55);
// By then the offer request was over and its callback delivered, so the send was accepted
ASSERT_EQ(mPeerDelegate.mSessionsAssigned, 1);
EXPECT_FALSE(mPeerDelegate.mRequestInFlightAtAssign);
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mPeerDelegate.mSendResultAtAssign, CHIP_NO_ERROR);
EXPECT_EQ(mClient.mConnectRequests, 2); // the offer, then the candidates
// The candidate send is the request now in flight
ASSERT_EQ(mClient.SendPendingICECandidates(), CHIP_NO_ERROR);
EXPECT_EQ(mClient.mSentIceSessionId, 55);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, OfferPayloadFollowsTheNormalFlow)
{
EstablishSessionWithId(55);
ASSERT_EQ(mClient.mSendAttempts, 1);
EXPECT_TRUE(mClient.mSentSessionIdWasNull); // A new session is the camera's to assign
EXPECT_EQ(mClient.mSentSdp, "v=0 test offer");
EXPECT_EQ(mClient.mSentUsage, Globals::StreamUsageEnum::kAnalysis);
EXPECT_EQ(mClient.mSentOriginatingEndpoint, kRequestorEndpoint); // Where the requestor cluster is registered
ASSERT_EQ(mClient.mSentVideoStreams.size(), 1u);
EXPECT_EQ(mClient.mSentVideoStreams[0], kVideoStreamId);
EXPECT_TRUE(mClient.mSentDeprecatedStreamIdAbsent);
EXPECT_TRUE(mClient.mSentAudioAbsent); // No audio for analysis
EXPECT_TRUE(mClient.mSentIceAbsent); // ICE configuration is the camera's default
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, CameraErrorStatusIsPropagatedVerbatim)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
// The camera refuses the offer with a specific status
mClient.OnError(mClient.Sender(), StatusIB(Status::ResourceExhausted).ToChipError());
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::ResourceExhausted);
// The camera answered, so the offer had reached it: the stream this activates is failed, not
// left as it was
EXPECT_TRUE(mCallback.mLastOfferSent);
EXPECT_EQ(mPeerDelegate.mSessionsAssigned, 0);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ATransportErrorIsReportedAsFailure)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
// No camera status behind the error: the exchange timed out. Recorded as a failure at once
mClient.OnError(mClient.Sender(), CHIP_ERROR_TIMEOUT);
EXPECT_EQ(mClient.CurrentRequest().GetPhase(), InterceptingWebRTCClient::Request::Phase::kFailed);
EXPECT_EQ(mClient.CurrentRequest().FailureStatus(), Status::Failure);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 1);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnOfferTheClientCannotSendFailsTheRequest)
{
mClient.mSendResult = CHIP_ERROR_NO_MEMORY;
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
// Nothing left this node, so no OnDone will come: the request fails right here
EXPECT_EQ(mClient.mSendAttempts, 1);
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
// And the offer never reached the camera, so the stream it would have activated is untouched
EXPECT_FALSE(mCallback.mLastOfferSent);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 1);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
// Completed: the client accepts a new request again
mClient.mSendResult = CHIP_NO_ERROR;
EstablishSessionWithId(55);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnOfferThatFailsAfterBeingRecordedIsRolledBack)
{
// The send fails once the request has been advanced and the sender recorded, as it does when
// AddRequestData or SendCommandRequest fails: the phase and the sender have to be undone, or
// the client stays wedged in kInvoking with a sender that will never call back.
mClient.mSendResultAfterRecording = CHIP_ERROR_INCORRECT_STATE;
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 1);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
// The rollback is what makes this succeed: a request left in kInvoking would answer BUSY
mClient.mSendResultAfterRecording = CHIP_NO_ERROR;
EstablishSessionWithId(55);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnEndSessionThatFailsAfterBeingRecordedIsRolledBack)
{
EstablishSessionWithId(55);
ASSERT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_NO_ERROR);
mClient.mSendResultAfterRecording = CHIP_ERROR_INCORRECT_STATE;
mClient.ContinueWithSession();
EXPECT_EQ(mCallback.mEndedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
mClient.mSendResultAfterRecording = CHIP_NO_ERROR;
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, kVideoStreamId, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, CallbacksOfAnotherSenderAreIgnored)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
// A sender this request never used speaks up: none of it is this request's outcome
CommandSender stranger{ nullptr, nullptr };
FeedOfferResponseFrom(&stranger, 99, kVideoStreamId);
mClient.OnError(&stranger, StatusIB(Status::ResourceExhausted).ToChipError());
mClient.OnDone(&stranger);
EXPECT_EQ(mClient.CurrentRequest().GetPhase(), InterceptingWebRTCClient::Request::Phase::kInvoking);
EXPECT_EQ(mCallback.mInitiatedCount, 0);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
// The request's own sender concludes it
FeedOfferResponse(55);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
EXPECT_EQ(mCallback.mLastSession, 55);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ACameraErrorIsDeliveredOnlyWhenTheExchangeCloses)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
// The error arrives while the sender is still alive: recorded, not yet delivered
mClient.OnError(mClient.Sender(), StatusIB(Status::ResourceExhausted).ToChipError());
EXPECT_EQ(mClient.CurrentRequest().GetPhase(), InterceptingWebRTCClient::Request::Phase::kFailed);
EXPECT_EQ(mCallback.mInitiatedCount, 0);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 0);
// The exchange closes: the recorded status is delivered and the offer abandoned
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::ResourceExhausted);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 1);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnUnansweredExchangeFailsTheRequest)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
// The exchange ends with neither a response nor an error
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ARequestWithNoFreeSessionSlotIsRefusedUpFront)
{
for (uint16_t id = 101; id < 101 + kMaxSessions; id++)
{
EstablishSessionWithId(id);
}
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
const int connectsBefore = mClient.mConnectRequests;
// Nothing is signaled toward the camera, so no session can be granted that this node cannot track
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, kVideoStreamId, mCallback), CHIP_ERROR_NO_MEMORY);
EXPECT_EQ(mClient.mConnectRequests, connectsBefore);
EXPECT_EQ(mCallback.mInitiatedCount, kMaxSessions);
EXPECT_EQ(mPeerDelegate.mOffersRequested, kMaxSessions);
// A released session frees a slot for the next request
mClient.NotifyFailed(kCameraNode, 101);
EstablishSessionWithId(200);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), static_cast<size_t>(kMaxSessions));
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, EndSessionPayloadNamesTheSessionWithUserHangup)
{
EstablishSessionWithId(55);
DriveToEndSessionSent(55);
ASSERT_EQ(mClient.mEndSendAttempts, 1);
EXPECT_EQ(mClient.mSentEndSessionId, 55);
EXPECT_EQ(mClient.mSentEndReason, Globals::WebRTCEndReasonEnum::kUserHangup);
EXPECT_EQ(mCallback.mEndedCount, 0);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ConfirmedEndReleasesTheSessionEverywhere)
{
EstablishSessionWithId(55);
DriveToEndSessionSent(55);
FeedEndSessionStatus(Status::Success);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mEndedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
EXPECT_EQ(mCallback.mLastSession, 55);
// Gone from the requestor cluster, released by the application, no longer tracked
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
EXPECT_EQ(mPeerDelegate.mLastClosed, 55);
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_ERROR_INVALID_ARGUMENT);
// The slot is reusable
EstablishSessionWithId(56);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, CameraErrorOnEndSessionIsPropagatedAndTheSessionIsStillReleased)
{
EstablishSessionWithId(55);
DriveToEndSessionSent(55);
// The camera refuses
mClient.OnError(mClient.Sender(), StatusIB(Status::NotFound).ToChipError());
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mEndedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::NotFound);
// The stream goes to Failure, from which nothing could end this session again: released
// everywhere rather than left occupying a slot
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
EXPECT_EQ(mPeerDelegate.mLastClosed, 55);
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_ERROR_INVALID_ARGUMENT);
// The slot is reusable by the re-activation that follows
EstablishSessionWithId(56);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnErrorStatusInTheEndSessionResponseFailsIt)
{
EstablishSessionWithId(55);
DriveToEndSessionSent(55);
// A status-only response that is not SUCCESS is no answer the request can use
FeedEndSessionStatus(Status::Failure);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mEndedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, EndingASessionWhileAnotherRequestIsInFlightIsBusy)
{
EstablishSessionWithId(55);
EstablishSessionWithId(56);
ASSERT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_NO_ERROR);
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 56, mCallback), CHIP_ERROR_BUSY);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 2u); // Neither session was touched
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnEndSessionConcludingAfterTheCamerasEndReleasesOnce)
{
EstablishSessionWithId(55);
DriveToEndSessionSent(55);
// The camera ends the session on its own before answering our EndSession
mClient.NotifyEnded(kCameraNode, 55);
EXPECT_EQ(mCallback.mFailedCount, 1);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
// Our EndSession is then answered: reported, with nothing left to release
FeedEndSessionStatus(Status::Success);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mEndedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnUnansweredEndSessionExchangeFailsAndStillReleasesTheSession)
{
EstablishSessionWithId(55);
DriveToEndSessionSent(55);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mEndedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ConnectedSessionIsReportedActiveAndStaysTracked)
{
EstablishSessionWithId(55);
mClient.NotifyConnected(kCameraNode, 55);
EXPECT_EQ(mCallback.mActiveCount, 1);
EXPECT_EQ(mCallback.mLastSession, 55);
EXPECT_EQ(mCallback.mFailedCount, 0);
// Active is not over: still recorded, still held by the application, still endable
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 1u);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 0);
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AFailureReportedFromWithinOnSessionClosedReleasesOnce)
{
mPeerDelegate.mReportFailureOnClose = true;
EstablishSessionWithId(55);
// The camera's End releases the session; the application's close handler reports the same
// session failed while the release is in progress
mClient.NotifyEnded(kCameraNode, 55);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
EXPECT_EQ(mCallback.mFailedCount, 1);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AReissuedSessionIdFailsTheSessionStillTrackedUnderIt)
{
// A camera restarted and grants id 0 again while we still track its earlier session 0
EstablishSessionWithId(0);
EstablishSessionWithId(0);
// The earlier session is failed and released; the new one is the only one tracked
EXPECT_EQ(mCallback.mInitiatedCount, 2);
EXPECT_EQ(mCallback.mFailedCount, 1);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
EXPECT_EQ(mPeerDelegate.mSessionsAssigned, 2);
ASSERT_EQ(mRequestorCluster.GetCurrentSessions().size(), 1u);
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 0, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnErrorAfterTheResponseDoesNotUndoIt)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
FeedOfferResponse(55);
// A stray error IB after the successful response is ignored
mClient.OnError(mClient.Sender(), StatusIB(Status::Failure).ToChipError());
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
EXPECT_EQ(mCallback.mLastSession, 55);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 1u);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AResponseFromAnotherEndpointFailsTheRequest)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
using Fields = WebRTCTransportProvider::Commands::ProvideOfferResponse::Fields;
uint8_t buffer[64];
TLV::TLVWriter writer;
writer.Init(buffer);
TLV::TLVType containerType;
ASSERT_EQ(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, containerType), CHIP_NO_ERROR);
ASSERT_EQ(writer.Put(TLV::ContextTag(Fields::kWebRTCSessionID), static_cast<uint16_t>(55)), CHIP_NO_ERROR);
ASSERT_EQ(writer.EndContainer(containerType), CHIP_NO_ERROR);
TLV::TLVReader reader;
reader.Init(buffer, writer.GetLengthWritten());
ASSERT_EQ(reader.Next(), CHIP_NO_ERROR);
ConcreteCommandPath otherEndpoint(static_cast<EndpointId>(kProviderEndpoint + 1), WebRTCTransportProvider::Id,
WebRTCTransportProvider::Commands::ProvideOfferResponse::Id);
mClient.OnResponse(mClient.Sender(), otherEndpoint, StatusIB(), &reader);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, CameraEndedSessionIsReportedFailedAndReleased)
{
EstablishSessionWithId(55);
// The camera's End arrives on the requestor cluster, not as the outcome of our EndSession
mClient.NotifyEnded(kCameraNode, 55);
EXPECT_EQ(mCallback.mFailedCount, 1);
EXPECT_EQ(mCallback.mLastSession, 55);
EXPECT_EQ(mCallback.mEndedCount, 0);
// Released everywhere: no record, no peer connection, no longer endable
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
EXPECT_EQ(mPeerDelegate.mLastClosed, 55);
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_ERROR_INVALID_ARGUMENT);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, MediaFailureIsReportedFailedAndReleased)
{
EstablishSessionWithId(55);
mClient.NotifyFailed(kCameraNode, 55);
EXPECT_EQ(mCallback.mFailedCount, 1);
EXPECT_EQ(mCallback.mLastSession, 55);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
// The slot is reusable
EstablishSessionWithId(56);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, SignalsForUntrackedSessionsAreIgnored)
{
EstablishSessionWithId(55);
mClient.NotifyConnected(kCameraNode, 99);
mClient.NotifyFailed(kCameraNode, 99);
mClient.NotifyEnded(kCameraNode, 99);
EXPECT_EQ(mCallback.mActiveCount, 0);
EXPECT_EQ(mCallback.mFailedCount, 0);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 0);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 1u);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, SignalsRouteToTheCallbackThatInitiatedTheSession)
{
RecordingCallback otherCallback;
EstablishSessionWithId(55);
EstablishSessionWithId(56, otherCallback);
ASSERT_EQ(otherCallback.mInitiatedCount, 1);
mClient.NotifyConnected(kCameraNode, 56);
mClient.NotifyEnded(kCameraNode, 55);
EXPECT_EQ(otherCallback.mActiveCount, 1);
EXPECT_EQ(otherCallback.mFailedCount, 0);
EXPECT_EQ(mCallback.mActiveCount, 0);
EXPECT_EQ(mCallback.mFailedCount, 1);
EXPECT_EQ(mCallback.mLastSession, 55);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, SessionsOfDifferentCamerasSharingAnIdAreToldApart)
{
// Session ids are assigned per camera: two cameras both hand out id 0
const ScopedNodeId kOtherCamera(0x5678, 1);
RecordingCallback otherCallback;
EstablishSessionWithId(0, mCallback, kCameraNode);
EstablishSessionWithId(0, otherCallback, kOtherCamera);
ASSERT_EQ(mRequestorCluster.GetCurrentSessions().size(), 2u);
// The other camera's session fails: only it is released and reported
mClient.NotifyFailed(kOtherCamera, 0);
EXPECT_EQ(otherCallback.mFailedCount, 1);
EXPECT_EQ(otherCallback.mLastCamera, kOtherCamera);
EXPECT_EQ(mCallback.mFailedCount, 0);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
EXPECT_EQ(mPeerDelegate.mLastClosedCamera, kOtherCamera);
ASSERT_EQ(mRequestorCluster.GetCurrentSessions().size(), 1u);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions()[0].peerNodeID, kCameraNode.GetNodeId());
// The first camera's session is intact: connected, and still endable under its own camera
mClient.NotifyConnected(kCameraNode, 0);
EXPECT_EQ(mCallback.mActiveCount, 1);
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 0, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnEstablishedSessionIsNotAnAbandonedOffer)
{
EstablishSessionWithId(55);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 0);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnOfferLeftUnansweredIsAbandoned)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 1);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnOfferTheApplicationCouldNotProduceIsAbandoned)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_ERROR_INTERNAL, CharSpan());
// The application may have created the peer connection before failing to describe it
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 1);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ARequestFailingBeforeTheOfferAbandonsNothing)
{
// No WebRTCTransportProvider on the endpoint: the application was never asked for an offer
ASSERT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, kVideoStreamId, mCallback), CHIP_NO_ERROR);
mClient.EnterProviderCheck();
mClient.OnDone(static_cast<ReadClient *>(nullptr));
EXPECT_EQ(mCallback.mLastStatus, Status::NotFound);
EXPECT_EQ(mPeerDelegate.mOffersRequested, 0);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 0);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, CancelAbandonsThePendingOffer)
{
DriveToOffer();
ASSERT_EQ(mPeerDelegate.mOffersRequested, 1);
mClient.Cancel();
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 1);
EXPECT_EQ(mCallback.mInitiatedCount, 0);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, CancelReleasesEveryTrackedSessionSilently)
{
EstablishSessionWithId(55);
EstablishSessionWithId(56);
mClient.Cancel();
// Released everywhere, with no outcome delivered
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 2);
EXPECT_EQ(mCallback.mFailedCount, 0);
EXPECT_EQ(mCallback.mEndedCount, 0);
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_ERROR_INVALID_ARGUMENT);
// Late signals for the forgotten sessions are ignored
mClient.NotifyConnected(kCameraNode, 55);
mClient.NotifyEnded(kCameraNode, 56);
EXPECT_EQ(mCallback.mActiveCount, 0);
EXPECT_EQ(mCallback.mFailedCount, 0);
// The client is fully reusable
EstablishSessionWithId(57);
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ARequestBeforeInitIsRefused)
{
InterceptingWebRTCClient uninitialised;
EXPECT_EQ(uninitialised.RequestSession(kCameraNode, kProviderEndpoint, kVideoStreamId, mCallback), CHIP_ERROR_INCORRECT_STATE);
EXPECT_EQ(uninitialised.mConnectRequests, 0);
// With no session table nothing is tracked, so the session-bound entry points find nothing to
// act on and walk no memory
RecordingCallback uninitialisedCallback;
EXPECT_NE(uninitialised.EndSession(kCameraNode, kProviderEndpoint, 55, uninitialisedCallback), CHIP_NO_ERROR);
auto candidates = TwoCandidates();
EXPECT_NE(uninitialised.SendICECandidates(
kCameraNode, 55, Span<const Globals::Structs::ICECandidateStruct::Type>(candidates.data(), candidates.size())),
CHIP_NO_ERROR);
uninitialised.NotifyConnected(kCameraNode, 55);
uninitialised.NotifyFailed(kCameraNode, 55);
uninitialised.NotifyEnded(kCameraNode, 55);
uninitialised.Cancel();
EXPECT_EQ(uninitialisedCallback.mInitiatedCount, 0);
EXPECT_EQ(uninitialisedCallback.mActiveCount, 0);
EXPECT_EQ(uninitialisedCallback.mFailedCount, 0);
EXPECT_EQ(uninitialisedCallback.mEndedCount, 0);
EXPECT_EQ(uninitialised.mConnectRequests, 0);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnEmptyOfferFailsTheRequest)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, CharSpan());
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mClient.mSendAttempts, 0);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 1);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, TheRequestorRecordCarriesTheOfferedVideoStreamId)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
FeedOfferResponse(55, std::nullopt);
mClient.OnDone(mClient.Sender());
auto sessions = mRequestorCluster.GetCurrentSessions();
ASSERT_EQ(sessions.size(), 1u);
ASSERT_FALSE(sessions[0].videoStreamID.IsNull());
EXPECT_EQ(sessions[0].videoStreamID.Value(), kVideoStreamId);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, TheRequestorRecordCarriesTheStreamInVideoStreams)
{
EstablishSessionWithId(55);
// An established session's streams are read from VideoStreams, not the deprecated scalar
auto sessions = mRequestorCluster.GetCurrentSessions();
ASSERT_EQ(sessions.size(), 1u);
ASSERT_TRUE(sessions[0].videoStreams.HasValue());
ASSERT_EQ(sessions[0].videoStreams.Value().size(), 1u);
EXPECT_EQ(sessions[0].videoStreams.Value().data()[0], kVideoStreamId);
EXPECT_TRUE(sessions[0].audioStreams.HasValue() == false);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, TheResponsesDeprecatedVideoStreamIdIsIgnored)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
FeedOfferResponse(55, static_cast<uint16_t>(kVideoStreamId + 1));
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mLastStatus, Status::Success);
auto sessions = mRequestorCluster.GetCurrentSessions();
ASSERT_EQ(sessions.size(), 1u);
ASSERT_FALSE(sessions[0].videoStreamID.IsNull());
EXPECT_EQ(sessions[0].videoStreamID.Value(), kVideoStreamId);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ASecondOfferResponseIsIgnored)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
FeedOfferResponse(55);
FeedOfferResponse(56); // rejected: the command was already responded to
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastSession, 55);
EXPECT_EQ(mPeerDelegate.mLastAssigned, 55);
ASSERT_EQ(mRequestorCluster.GetCurrentSessions().size(), 1u);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions()[0].id, 55);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnOfferResponseWithoutDataFailsTheRequest)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
ConcreteCommandPath responsePath(kProviderEndpoint, WebRTCTransportProvider::Id,
WebRTCTransportProvider::Commands::ProvideOfferResponse::Id);
mClient.OnResponse(mClient.Sender(), responsePath, StatusIB(), nullptr);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 1);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AResponseOnTheWrongCommandFailsTheRequest)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
// A well-formed ProvideOfferResponse payload arriving under another command id
using Fields = WebRTCTransportProvider::Commands::ProvideOfferResponse::Fields;
uint8_t buffer[64];
TLV::TLVWriter writer;
writer.Init(buffer);
TLV::TLVType containerType;
ASSERT_EQ(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, containerType), CHIP_NO_ERROR);
ASSERT_EQ(writer.Put(TLV::ContextTag(Fields::kWebRTCSessionID), static_cast<uint16_t>(55)), CHIP_NO_ERROR);
ASSERT_EQ(writer.EndContainer(containerType), CHIP_NO_ERROR);
TLV::TLVReader reader;
reader.Init(buffer, writer.GetLengthWritten());
ASSERT_EQ(reader.Next(), CHIP_NO_ERROR);
ConcreteCommandPath wrongPath(kProviderEndpoint, WebRTCTransportProvider::Id,
WebRTCTransportProvider::Commands::SolicitOfferResponse::Id);
mClient.OnResponse(mClient.Sender(), wrongPath, StatusIB(), &reader);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnUndecodableOfferResponseFailsTheRequest)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
// A scalar where a structure is expected
uint8_t buffer[8];
TLV::TLVWriter writer;
writer.Init(buffer, sizeof(buffer));
ASSERT_EQ(writer.Put(TLV::AnonymousTag(), static_cast<uint32_t>(7)), CHIP_NO_ERROR);
TLV::TLVReader reader;
reader.Init(buffer, writer.GetLengthWritten());
ASSERT_EQ(reader.Next(), CHIP_NO_ERROR);
ConcreteCommandPath responsePath(kProviderEndpoint, WebRTCTransportProvider::Id,
WebRTCTransportProvider::Commands::ProvideOfferResponse::Id);
mClient.OnResponse(mClient.Sender(), responsePath, StatusIB(), &reader);
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mInitiatedCount, 1);
EXPECT_EQ(mCallback.mLastStatus, Status::Failure);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 1);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, CancelDuringTheOfferExchangeAbandonsTheOffer)
{
DriveToOffer();
ASSERT_NE(mPeerDelegate.mLastOfferCallback, nullptr);
mPeerDelegate.mLastOfferCallback->OnOfferReady(CHIP_NO_ERROR, "v=0 test offer"_span);
ASSERT_EQ(mClient.mSendAttempts, 1);
mClient.Cancel();
EXPECT_EQ(mCallback.mInitiatedCount, 0);
EXPECT_EQ(mPeerDelegate.mOffersAbandoned, 1);
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, kVideoStreamId, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, CancelDuringAnEndSessionDeliversNothingAndForgetsTheSession)
{
EstablishSessionWithId(55);
DriveToEndSessionSent(55);
mClient.Cancel();
EXPECT_EQ(mCallback.mEndedCount, 0);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 0u);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 1);
EXPECT_EQ(mClient.RequestSession(kCameraNode, kProviderEndpoint, kVideoStreamId, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnUnansweredCandidateSendKeepsTheSession)
{
EstablishSessionWithId(55);
DriveToICECandidatesSent(55);
// The exchange closes with no response at all
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mFailedCount, 0);
EXPECT_EQ(mCallback.mEndedCount, 0);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 1u);
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ICECandidatesForAnUntrackedSessionAreRejected)
{
auto candidates = TwoCandidates();
EXPECT_EQ(mClient.SendICECandidates(
kCameraNode, 99, Span<const Globals::Structs::ICECandidateStruct::Type>(candidates.data(), candidates.size())),
CHIP_ERROR_INVALID_ARGUMENT);
EXPECT_EQ(mClient.mConnectRequests, 0);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AnEmptyCandidateListIsRejected)
{
EstablishSessionWithId(55);
EXPECT_EQ(mClient.SendICECandidates(kCameraNode, 55, Span<const Globals::Structs::ICECandidateStruct::Type>()),
CHIP_ERROR_INVALID_ARGUMENT);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ICECandidatesAreCopiedAndSentToTheSessionsCamera)
{
EstablishSessionWithId(55);
const int connectsBefore = mClient.mConnectRequests;
{
// The caller's list dies before the send; the client must have copied it
auto candidates = TwoCandidates();
ASSERT_EQ(
mClient.SendICECandidates(kCameraNode, 55,
Span<const Globals::Structs::ICECandidateStruct::Type>(candidates.data(), candidates.size())),
CHIP_NO_ERROR);
}
ASSERT_EQ(mClient.SendPendingICECandidates(), CHIP_NO_ERROR);
// Routed through the tracked session's camera
EXPECT_EQ(mClient.mConnectRequests, connectsBefore + 1);
EXPECT_EQ(mClient.mLastPeer, kCameraNode);
ASSERT_EQ(mClient.mIceSendAttempts, 1);
EXPECT_EQ(mClient.mSentIceSessionId, 55);
ASSERT_EQ(mClient.mSentCandidates.size(), 2u);
EXPECT_EQ(mClient.mSentCandidates[0], "candidate:1 1 UDP 2122252543 192.168.1.10 5000 typ host");
EXPECT_EQ(mClient.mSentMids[0].value_or(""), "video");
ASSERT_FALSE(mClient.mSentMLineIndexes[0].IsNull());
EXPECT_EQ(mClient.mSentMLineIndexes[0].Value(), 0);
EXPECT_EQ(mClient.mSentCandidates[1], "candidate:2 1 UDP 1686052607 203.0.113.5 5000 typ srflx");
EXPECT_FALSE(mClient.mSentMids[1].has_value());
EXPECT_TRUE(mClient.mSentMLineIndexes[1].IsNull());
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ICECandidatesAreRoutedToTheSessionsOwnCamera)
{
// Two cameras, each with a session 0
const ScopedNodeId kOtherCamera(0x5678, 1);
RecordingCallback otherCallback;
EstablishSessionWithId(0, mCallback, kCameraNode);
EstablishSessionWithId(0, otherCallback, kOtherCamera);
auto candidates = TwoCandidates();
ASSERT_EQ(mClient.SendICECandidates(
kOtherCamera, 0, Span<const Globals::Structs::ICECandidateStruct::Type>(candidates.data(), candidates.size())),
CHIP_NO_ERROR);
// The send reaches the camera that assigned the session, not the first camera with that id
EXPECT_EQ(mClient.mLastPeer, kOtherCamera);
ASSERT_EQ(mClient.SendPendingICECandidates(), CHIP_NO_ERROR);
ASSERT_EQ(mClient.mIceSendAttempts, 1);
EXPECT_EQ(mClient.mSentIceSessionId, 0);
EXPECT_EQ(mClient.mSentCandidates.size(), 2u);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, AConfirmedCandidateSendCompletesSilentlyAndFreesTheClient)
{
EstablishSessionWithId(55);
DriveToICECandidatesSent(55);
const int initiatedBefore = mCallback.mInitiatedCount;
ConcreteCommandPath responsePath(kProviderEndpoint, WebRTCTransportProvider::Id,
WebRTCTransportProvider::Commands::ProvideICECandidates::Id);
mClient.OnResponse(mClient.Sender(), responsePath, StatusIB(), nullptr);
mClient.OnDone(mClient.Sender());
// No callback is owed for candidates
EXPECT_EQ(mCallback.mInitiatedCount, initiatedBefore);
EXPECT_EQ(mCallback.mEndedCount, 0);
EXPECT_EQ(mCallback.mFailedCount, 0);
// The session is untouched and the client accepts the next request
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 1u);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 0);
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, ACameraErrorOnCandidatesIsLoggedOnlyAndKeepsTheSession)
{
EstablishSessionWithId(55);
DriveToICECandidatesSent(55);
mClient.OnError(mClient.Sender(), StatusIB(Status::NotFound).ToChipError());
mClient.OnDone(mClient.Sender());
EXPECT_EQ(mCallback.mEndedCount, 0);
EXPECT_EQ(mCallback.mFailedCount, 0);
EXPECT_EQ(mRequestorCluster.GetCurrentSessions().size(), 1u);
EXPECT_EQ(mPeerDelegate.mSessionsClosed, 0);
EXPECT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_NO_ERROR);
}
TEST_F(TestDefaultAvAnalysisWebRTCClient, CandidatesWhileAnotherRequestIsInFlightAreBusy)
{
EstablishSessionWithId(55);
ASSERT_EQ(mClient.EndSession(kCameraNode, kProviderEndpoint, 55, mCallback), CHIP_NO_ERROR);
auto candidates = TwoCandidates();
EXPECT_EQ(mClient.SendICECandidates(
kCameraNode, 55, Span<const Globals::Structs::ICECandidateStruct::Type>(candidates.data(), candidates.size())),
CHIP_ERROR_BUSY);
}
} // namespace