Add MockMoqtSession.
The goal here is to allow MoQT applications to write tests without making assumptions about what happens on the wire (currently, the only way to write a test for MoqtSession is to mock out the WebTransport session underneath).
PiperOrigin-RevId: 741196294
diff --git a/build/source_list.bzl b/build/source_list.bzl
index b6c75a5..68fbf12 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -1548,6 +1548,7 @@
"quic/moqt/moqt_session_interface.h",
"quic/moqt/moqt_subscribe_windows.h",
"quic/moqt/moqt_track.h",
+ "quic/moqt/test_tools/mock_moqt_session.h",
"quic/moqt/test_tools/moqt_framer_utils.h",
"quic/moqt/test_tools/moqt_session_peer.h",
"quic/moqt/test_tools/moqt_simulator_harness.h",
@@ -1585,6 +1586,8 @@
"quic/moqt/moqt_subscribe_windows_test.cc",
"quic/moqt/moqt_track.cc",
"quic/moqt/moqt_track_test.cc",
+ "quic/moqt/test_tools/mock_moqt_session.cc",
+ "quic/moqt/test_tools/mock_moqt_session_test.cc",
"quic/moqt/test_tools/moqt_framer_utils.cc",
"quic/moqt/test_tools/moqt_simulator_harness.cc",
"quic/moqt/tools/chat_client.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index d66f724..ccf396b 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -1552,6 +1552,7 @@
"src/quiche/quic/moqt/moqt_session_interface.h",
"src/quiche/quic/moqt/moqt_subscribe_windows.h",
"src/quiche/quic/moqt/moqt_track.h",
+ "src/quiche/quic/moqt/test_tools/mock_moqt_session.h",
"src/quiche/quic/moqt/test_tools/moqt_framer_utils.h",
"src/quiche/quic/moqt/test_tools/moqt_session_peer.h",
"src/quiche/quic/moqt/test_tools/moqt_simulator_harness.h",
@@ -1589,6 +1590,8 @@
"src/quiche/quic/moqt/moqt_subscribe_windows_test.cc",
"src/quiche/quic/moqt/moqt_track.cc",
"src/quiche/quic/moqt/moqt_track_test.cc",
+ "src/quiche/quic/moqt/test_tools/mock_moqt_session.cc",
+ "src/quiche/quic/moqt/test_tools/mock_moqt_session_test.cc",
"src/quiche/quic/moqt/test_tools/moqt_framer_utils.cc",
"src/quiche/quic/moqt/test_tools/moqt_simulator_harness.cc",
"src/quiche/quic/moqt/tools/chat_client.cc",
diff --git a/build/source_list.json b/build/source_list.json
index 8ae8fe1..d721fe7 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -1551,6 +1551,7 @@
"quiche/quic/moqt/moqt_session_interface.h",
"quiche/quic/moqt/moqt_subscribe_windows.h",
"quiche/quic/moqt/moqt_track.h",
+ "quiche/quic/moqt/test_tools/mock_moqt_session.h",
"quiche/quic/moqt/test_tools/moqt_framer_utils.h",
"quiche/quic/moqt/test_tools/moqt_session_peer.h",
"quiche/quic/moqt/test_tools/moqt_simulator_harness.h",
@@ -1588,6 +1589,8 @@
"quiche/quic/moqt/moqt_subscribe_windows_test.cc",
"quiche/quic/moqt/moqt_track.cc",
"quiche/quic/moqt/moqt_track_test.cc",
+ "quiche/quic/moqt/test_tools/mock_moqt_session.cc",
+ "quiche/quic/moqt/test_tools/mock_moqt_session_test.cc",
"quiche/quic/moqt/test_tools/moqt_framer_utils.cc",
"quiche/quic/moqt/test_tools/moqt_simulator_harness.cc",
"quiche/quic/moqt/tools/chat_client.cc",
diff --git a/quiche/quic/moqt/test_tools/mock_moqt_session.cc b/quiche/quic/moqt/test_tools/mock_moqt_session.cc
new file mode 100644
index 0000000..cfd5b47
--- /dev/null
+++ b/quiche/quic/moqt/test_tools/mock_moqt_session.cc
@@ -0,0 +1,224 @@
+// Copyright 2025 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quiche/quic/moqt/test_tools/mock_moqt_session.h"
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "quiche/quic/moqt/moqt_failed_fetch.h"
+#include "quiche/quic/moqt/moqt_messages.h"
+#include "quiche/quic/moqt/moqt_priority.h"
+#include "quiche/quic/moqt/moqt_publisher.h"
+#include "quiche/quic/moqt/moqt_subscribe_windows.h"
+#include "quiche/quic/moqt/moqt_track.h"
+#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/platform/api/quiche_test.h"
+#include "quiche/web_transport/web_transport.h"
+
+namespace moqt::test {
+
+namespace {
+using ::testing::_;
+}
+
+// Object listener that forwards all of the objects to the
+// SubcribeRemoteTrack::Visitor provided.
+class MockMoqtSession::LoopbackObjectListener : public MoqtObjectListener {
+ public:
+ LoopbackObjectListener(FullTrackName name,
+ SubscribeRemoteTrack::Visitor* visitor,
+ std::shared_ptr<MoqtTrackPublisher> publisher,
+ SubscribeWindow window)
+ : name_(name),
+ visitor_(visitor),
+ publisher_(std::move(publisher)),
+ window_(std::move(window)) {
+ publisher_->AddObjectListener(this);
+ }
+ ~LoopbackObjectListener() { publisher_->RemoveObjectListener(this); }
+
+ LoopbackObjectListener(const LoopbackObjectListener&) = delete;
+ LoopbackObjectListener(LoopbackObjectListener&&) = delete;
+ LoopbackObjectListener& operator=(const LoopbackObjectListener&) = delete;
+ LoopbackObjectListener& operator=(LoopbackObjectListener&&) = delete;
+
+ void OnSubscribeAccepted() override {
+ visitor_->OnReply(name_,
+ HasObjects()
+ ? std::make_optional(publisher_->GetLargestSequence())
+ : std::nullopt,
+ std::nullopt);
+ }
+
+ void OnSubscribeRejected(MoqtSubscribeErrorReason reason,
+ std::optional<uint64_t> track_alias) {
+ visitor_->OnReply(name_, std::nullopt, reason.reason_phrase);
+ }
+
+ void OnNewObjectAvailable(FullSequence sequence) {
+ std::optional<PublishedObject> object =
+ publisher_->GetCachedObject(sequence);
+ if (!object.has_value()) {
+ QUICHE_LOG(FATAL)
+ << "GetCachedObject() returned nullopt for a sequence passed into "
+ "OnNewObjectAvailable()";
+ return;
+ }
+ if (!window_.InWindow(object->sequence)) {
+ return;
+ }
+ visitor_->OnObjectFragment(name_, sequence, object->publisher_priority,
+ object->status, object->payload.AsStringView(),
+ /*end_of_message=*/true);
+ }
+
+ void OnNewFinAvailable(FullSequence sequence) override {}
+ void OnSubgroupAbandoned(FullSequence sequence,
+ webtransport::StreamErrorCode error_code) override {}
+ void OnGroupAbandoned(uint64_t group_id) override {}
+ void OnTrackPublisherGone() override { visitor_->OnSubscribeDone(name_); }
+
+ private:
+ bool HasObjects() {
+ absl::StatusOr<MoqtTrackStatusCode> status = publisher_->GetTrackStatus();
+ if (!status.ok()) {
+ return false;
+ }
+ return *status == MoqtTrackStatusCode::kInProgress ||
+ *status == MoqtTrackStatusCode::kFinished;
+ }
+
+ FullTrackName name_;
+ SubscribeRemoteTrack::Visitor* visitor_;
+ std::shared_ptr<MoqtTrackPublisher> publisher_;
+ SubscribeWindow window_;
+};
+
+bool MockMoqtSession::Subscribe(const FullTrackName& name,
+ SubscribeRemoteTrack::Visitor* visitor,
+ SubscribeWindow window) {
+ auto track_publisher = publisher_->GetTrack(name);
+ if (!track_publisher.ok()) {
+ visitor->OnReply(name, std::nullopt, track_publisher.status().ToString());
+ return false;
+ }
+ auto [it, inserted] = receiving_subscriptions_.insert(
+ {name,
+ std::make_unique<LoopbackObjectListener>(
+ name, visitor, *std::move(track_publisher), std::move(window))});
+ return inserted;
+}
+
+MockMoqtSession::MockMoqtSession(MoqtPublisher* publisher)
+ : publisher_(publisher) {
+ ON_CALL(*this, Error)
+ .WillByDefault([](MoqtError code, absl::string_view error) {
+ ADD_FAILURE() << "Unhandled MoQT fatal error, with code "
+ << static_cast<int>(code) << " and message: " << error;
+ });
+ if (publisher_ != nullptr) {
+ ON_CALL(*this, SubscribeCurrentObject)
+ .WillByDefault([this](const FullTrackName& name,
+ SubscribeRemoteTrack::Visitor* visitor,
+ MoqtSubscribeParameters) {
+ return Subscribe(name, visitor, SubscribeWindow(0, 0));
+ });
+ ON_CALL(*this, SubscribeAbsolute(_, _, _, _, _))
+ .WillByDefault([this](const FullTrackName& name, uint64_t start_group,
+ uint64_t start_object,
+ SubscribeRemoteTrack::Visitor* visitor,
+ MoqtSubscribeParameters) {
+ return Subscribe(name, visitor,
+ SubscribeWindow(start_group, start_object));
+ });
+ ON_CALL(*this, SubscribeAbsolute(_, _, _, _, _, _))
+ .WillByDefault([this](const FullTrackName& name, uint64_t start_group,
+ uint64_t start_object, uint64_t end_group,
+ SubscribeRemoteTrack::Visitor* visitor,
+ MoqtSubscribeParameters) {
+ return Subscribe(name, visitor,
+ SubscribeWindow(start_group, start_object, end_group,
+ UINT64_MAX));
+ });
+ ON_CALL(*this, Unsubscribe)
+ .WillByDefault([this](const FullTrackName& name) {
+ receiving_subscriptions_.erase(name);
+ });
+ ON_CALL(*this, Fetch)
+ .WillByDefault(
+ [this](const FullTrackName& name, FetchResponseCallback callback,
+ FullSequence start, uint64_t end_group,
+ std::optional<uint64_t> end_object, MoqtPriority priority,
+ std::optional<MoqtDeliveryOrder> delivery_order,
+ MoqtSubscribeParameters parameters) {
+ auto track_publisher = publisher_->GetTrack(name);
+ if (!track_publisher.ok()) {
+ std::move(callback)(std::make_unique<MoqtFailedFetch>(
+ track_publisher.status()));
+ return true;
+ }
+ std::move(callback)(track_publisher->get()->Fetch(
+ start, end_group, end_object,
+ delivery_order.value_or(MoqtDeliveryOrder::kAscending)));
+ return true;
+ });
+ ON_CALL(*this, JoiningFetch(_, _, _, _))
+ .WillByDefault([this](const FullTrackName& name,
+ SubscribeRemoteTrack::Visitor* visitor,
+ uint64_t num_previous_groups,
+ MoqtSubscribeParameters parameters) {
+ return JoiningFetch(
+ name, visitor,
+ [name, visitor](std::unique_ptr<MoqtFetchTask> fetch) {
+ PublishedObject object;
+ while (fetch->GetNextObject(object) ==
+ MoqtFetchTask::kSuccess) {
+ visitor->OnObjectFragment(
+ name, object.sequence, object.publisher_priority,
+ object.status, object.payload.AsStringView(), true);
+ }
+ },
+ num_previous_groups, 0x80, std::nullopt, parameters);
+ });
+ ON_CALL(*this, JoiningFetch(_, _, _, _, _, _, _))
+ .WillByDefault([this](const FullTrackName& name,
+ SubscribeRemoteTrack::Visitor* visitor,
+ FetchResponseCallback callback,
+ uint64_t num_previous_groups,
+ MoqtPriority priority,
+ std::optional<MoqtDeliveryOrder> delivery_order,
+ MoqtSubscribeParameters parameters) {
+ SubscribeCurrentObject(name, visitor, parameters);
+ auto track_publisher = publisher_->GetTrack(name);
+ if (!track_publisher.ok()) {
+ std::move(callback)(
+ std::make_unique<MoqtFailedFetch>(track_publisher.status()));
+ return true;
+ }
+ if (track_publisher->get()->GetTrackStatus().value_or(
+ MoqtTrackStatusCode::kStatusNotAvailable) ==
+ MoqtTrackStatusCode::kNotYetBegun) {
+ return Fetch(name, std::move(callback), FullSequence(0, 0), 0, 0,
+ priority, delivery_order, std::move(parameters));
+ }
+ FullSequence largest = track_publisher->get()->GetLargestSequence();
+ uint64_t start_group = largest.group >= num_previous_groups
+ ? largest.group - num_previous_groups + 1
+ : 0;
+ return Fetch(name, std::move(callback), FullSequence(start_group, 0),
+ largest.group, largest.object, priority, delivery_order,
+ std::move(parameters));
+ });
+ }
+}
+
+MockMoqtSession::~MockMoqtSession() = default;
+
+} // namespace moqt::test
diff --git a/quiche/quic/moqt/test_tools/mock_moqt_session.h b/quiche/quic/moqt/test_tools/mock_moqt_session.h
new file mode 100644
index 0000000..d6eddb8
--- /dev/null
+++ b/quiche/quic/moqt/test_tools/mock_moqt_session.h
@@ -0,0 +1,102 @@
+// Copyright 2025 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef QUICHE_QUIC_MOQT_TEST_TOOLS_MOCK_MOQT_SESSION_H_
+#define QUICHE_QUIC_MOQT_TEST_TOOLS_MOCK_MOQT_SESSION_H_
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/strings/string_view.h"
+#include "quiche/quic/moqt/moqt_messages.h"
+#include "quiche/quic/moqt/moqt_priority.h"
+#include "quiche/quic/moqt/moqt_publisher.h"
+#include "quiche/quic/moqt/moqt_session_callbacks.h"
+#include "quiche/quic/moqt/moqt_session_interface.h"
+#include "quiche/quic/moqt/moqt_subscribe_windows.h"
+#include "quiche/quic/moqt/moqt_track.h"
+#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/platform/api/quiche_test.h"
+
+namespace moqt::test {
+
+// Mock version of MoqtSession. If `publisher` is provided via constructor, all
+// of the SUBSCRIBE and FETCH requests are routed towards it.
+class MockMoqtSession : public MoqtSessionInterface {
+ public:
+ explicit MockMoqtSession(MoqtPublisher* publisher);
+ ~MockMoqtSession() override;
+
+ MockMoqtSession(const MockMoqtSession&) = delete;
+ MockMoqtSession(MockMoqtSession&&) = delete;
+ MockMoqtSession& operator=(const MockMoqtSession&) = delete;
+ MockMoqtSession& operator=(MockMoqtSession&&) = delete;
+
+ MoqtSessionCallbacks& callbacks() override { return callbacks_; }
+
+ MOCK_METHOD(void, Error, (MoqtError code, absl::string_view error),
+ (override));
+ MOCK_METHOD(bool, SubscribeAbsolute,
+ (const FullTrackName& name, uint64_t start_group,
+ uint64_t start_object, SubscribeRemoteTrack::Visitor* visitor,
+ MoqtSubscribeParameters parameters),
+ (override));
+ MOCK_METHOD(bool, SubscribeAbsolute,
+ (const FullTrackName& name, uint64_t start_group,
+ uint64_t start_object, uint64_t end_group,
+ SubscribeRemoteTrack::Visitor* visitor,
+ MoqtSubscribeParameters parameters),
+ (override));
+ MOCK_METHOD(bool, SubscribeCurrentObject,
+ (const FullTrackName& name,
+ SubscribeRemoteTrack::Visitor* visitor,
+ MoqtSubscribeParameters parameters),
+ (override));
+ MOCK_METHOD(void, Unsubscribe, (const FullTrackName& name), (override));
+ MOCK_METHOD(bool, Fetch,
+ (const FullTrackName& name, FetchResponseCallback callback,
+ FullSequence start, uint64_t end_group,
+ std::optional<uint64_t> end_object, MoqtPriority priority,
+ std::optional<MoqtDeliveryOrder> delivery_order,
+ MoqtSubscribeParameters parameters),
+ (override));
+ MOCK_METHOD(bool, JoiningFetch,
+ (const FullTrackName& name,
+ SubscribeRemoteTrack::Visitor* visitor,
+ uint64_t num_previous_groups,
+ MoqtSubscribeParameters parameters),
+ (override));
+ MOCK_METHOD(bool, JoiningFetch,
+ (const FullTrackName& name,
+ SubscribeRemoteTrack::Visitor* visitor,
+ FetchResponseCallback callback, uint64_t num_previous_groups,
+ MoqtPriority priority,
+ std::optional<MoqtDeliveryOrder> delivery_order,
+ MoqtSubscribeParameters parameters),
+ (override));
+
+ [[deprecated]] bool SubscribeCurrentGroup(
+ const FullTrackName& name, SubscribeRemoteTrack::Visitor* visitor,
+ MoqtSubscribeParameters parameters) override {
+ QUICHE_LOG(FATAL) << "Don't call this";
+ }
+
+ private:
+ class LoopbackObjectListener;
+
+ bool Subscribe(const FullTrackName& name,
+ SubscribeRemoteTrack::Visitor* visitor,
+ SubscribeWindow window);
+
+ MoqtPublisher* const publisher_ = nullptr;
+ MoqtSessionCallbacks callbacks_;
+ absl::flat_hash_map<FullTrackName, std::unique_ptr<LoopbackObjectListener>>
+ receiving_subscriptions_;
+};
+
+} // namespace moqt::test
+
+#endif // QUICHE_QUIC_MOQT_TEST_TOOLS_MOCK_MOQT_SESSION_H_
diff --git a/quiche/quic/moqt/test_tools/mock_moqt_session_test.cc b/quiche/quic/moqt/test_tools/mock_moqt_session_test.cc
new file mode 100644
index 0000000..1ef2167
--- /dev/null
+++ b/quiche/quic/moqt/test_tools/mock_moqt_session_test.cc
@@ -0,0 +1,135 @@
+// Copyright 2025 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quiche/quic/moqt/test_tools/mock_moqt_session.h"
+
+#include <memory>
+#include <optional>
+#include <utility>
+
+#include "quiche/quic/moqt/moqt_known_track_publisher.h"
+#include "quiche/quic/moqt/moqt_messages.h"
+#include "quiche/quic/moqt/moqt_outgoing_queue.h"
+#include "quiche/quic/moqt/moqt_publisher.h"
+#include "quiche/quic/moqt/tools/moqt_mock_visitor.h"
+#include "quiche/quic/test_tools/quic_test_utils.h"
+#include "quiche/common/platform/api/quiche_test.h"
+
+namespace moqt::test {
+namespace {
+
+using ::testing::_;
+using ::testing::Eq;
+using ::testing::HasSubstr;
+using ::testing::Optional;
+
+FullTrackName TrackName() { return FullTrackName("foo", "bar"); }
+
+class MockMoqtSessionTest : public quiche::test::QuicheTest {
+ protected:
+ MockMoqtSessionTest() : session_(&publisher_) {
+ track_ = std::make_shared<MoqtOutgoingQueue>(
+ TrackName(), MoqtForwardingPreference::kSubgroup);
+ publisher_.Add(track_);
+ }
+
+ MoqtKnownTrackPublisher publisher_;
+ std::shared_ptr<MoqtOutgoingQueue> track_;
+ MockMoqtSession session_;
+};
+
+TEST_F(MockMoqtSessionTest, MissingTrack) {
+ testing::StrictMock<MockSubscribeRemoteTrackVisitor> visitor;
+ EXPECT_CALL(visitor,
+ OnReply(FullTrackName("doesn't", "exist"), Eq(std::nullopt),
+ Optional(HasSubstr("not found"))));
+ session_.SubscribeCurrentObject(FullTrackName("doesn't", "exist"), &visitor,
+ MoqtSubscribeParameters());
+}
+
+TEST_F(MockMoqtSessionTest, SubscribeCurrentObject) {
+ testing::StrictMock<MockSubscribeRemoteTrackVisitor> visitor;
+ EXPECT_CALL(visitor,
+ OnReply(TrackName(), Eq(std::nullopt), Eq(std::nullopt)));
+ session_.SubscribeCurrentObject(TrackName(), &visitor,
+ MoqtSubscribeParameters());
+ EXPECT_CALL(visitor, OnObjectFragment(TrackName(), FullSequence(0, 0), _, _,
+ "test", _));
+ track_->AddObject(quic::test::MemSliceFromString("test"), /*key=*/true);
+
+ session_.Unsubscribe(TrackName());
+ track_->AddObject(quic::test::MemSliceFromString("test2"), /*key=*/true);
+ // No visitor call here.
+}
+
+TEST_F(MockMoqtSessionTest, SubscribeAbsolute) {
+ testing::StrictMock<MockSubscribeRemoteTrackVisitor> visitor;
+ EXPECT_CALL(visitor,
+ OnReply(TrackName(), Eq(std::nullopt), Eq(std::nullopt)));
+ session_.SubscribeAbsolute(TrackName(), 1, 0, 1, &visitor,
+ MoqtSubscribeParameters());
+ EXPECT_CALL(visitor, OnObjectFragment(TrackName(), FullSequence(1, 0), _,
+ MoqtObjectStatus::kNormal, "b", _));
+ EXPECT_CALL(visitor, OnObjectFragment(TrackName(), FullSequence(1, 1), _,
+ MoqtObjectStatus::kEndOfGroup, "", _));
+ track_->AddObject(quic::test::MemSliceFromString("a"), /*key=*/true);
+ track_->AddObject(quic::test::MemSliceFromString("b"), /*key=*/true);
+ track_->AddObject(quic::test::MemSliceFromString("c"), /*key=*/true);
+}
+
+TEST_F(MockMoqtSessionTest, Fetch) {
+ track_->AddObject(quic::test::MemSliceFromString("a"), /*key=*/true);
+ track_->AddObject(quic::test::MemSliceFromString("b"), /*key=*/false);
+ track_->AddObject(quic::test::MemSliceFromString("c"), /*key=*/false);
+ track_->AddObject(quic::test::MemSliceFromString("d"), /*key=*/true);
+ std::unique_ptr<MoqtFetchTask> fetch;
+ session_.Fetch(
+ TrackName(),
+ [&](std::unique_ptr<MoqtFetchTask> new_fetch) {
+ fetch = std::move(new_fetch);
+ },
+ FullSequence(0, 1), 0, 2, 0x80, std::nullopt, MoqtSubscribeParameters());
+ PublishedObject object;
+ ASSERT_EQ(fetch->GetNextObject(object), MoqtFetchTask::kSuccess);
+ EXPECT_EQ(object.payload.AsStringView(), "b");
+ ASSERT_EQ(fetch->GetNextObject(object), MoqtFetchTask::kSuccess);
+ EXPECT_EQ(object.payload.AsStringView(), "c");
+ ASSERT_EQ(fetch->GetNextObject(object), MoqtFetchTask::kEof);
+}
+
+TEST_F(MockMoqtSessionTest, JoiningFetch) {
+ track_->AddObject(quic::test::MemSliceFromString("a"), /*key=*/true);
+ track_->AddObject(quic::test::MemSliceFromString("b"), /*key=*/true);
+ track_->AddObject(quic::test::MemSliceFromString("c"), /*key=*/true);
+ track_->AddObject(quic::test::MemSliceFromString("d"), /*key=*/true);
+
+ testing::StrictMock<MockSubscribeRemoteTrackVisitor> visitor;
+ EXPECT_CALL(visitor,
+ OnReply(TrackName(), Eq(FullSequence(3, 0)), Eq(std::nullopt)));
+ EXPECT_CALL(visitor, OnObjectFragment(TrackName(), FullSequence(2, 0), _,
+ MoqtObjectStatus::kNormal, "c", _));
+ EXPECT_CALL(visitor, OnObjectFragment(TrackName(), FullSequence(2, 1), _,
+ MoqtObjectStatus::kEndOfGroup, "", _));
+ EXPECT_CALL(visitor, OnObjectFragment(TrackName(), FullSequence(3, 0), _,
+ MoqtObjectStatus::kNormal, "d", _));
+ session_.JoiningFetch(TrackName(), &visitor, 2, MoqtSubscribeParameters());
+ EXPECT_CALL(visitor, OnObjectFragment(TrackName(), FullSequence(3, 1), _,
+ MoqtObjectStatus::kEndOfGroup, "", _));
+ EXPECT_CALL(visitor, OnObjectFragment(TrackName(), FullSequence(4, 0), _,
+ MoqtObjectStatus::kNormal, "e", _));
+ track_->AddObject(quic::test::MemSliceFromString("e"), /*key=*/true);
+}
+
+TEST_F(MockMoqtSessionTest, JoiningFetchNoObjects) {
+ testing::StrictMock<MockSubscribeRemoteTrackVisitor> visitor;
+ EXPECT_CALL(visitor,
+ OnReply(TrackName(), Eq(std::nullopt), Eq(std::nullopt)));
+ session_.JoiningFetch(TrackName(), &visitor, 0, MoqtSubscribeParameters());
+ EXPECT_CALL(visitor, OnObjectFragment(TrackName(), FullSequence(0, 0), _, _,
+ "test", _));
+ track_->AddObject(quic::test::MemSliceFromString("test"), /*key=*/true);
+}
+
+} // namespace
+} // namespace moqt::test