blob: e59cb951024ddb85c9889298f6111064936309e2 [file] [edit]
// Copyright (c) 2026 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/moqt_publish_stream.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <variant>
#include "absl/status/status.h"
#include "absl/strings/string_view.h"
#include "absl/types/span.h"
#include "quiche/quic/core/quic_alarm_factory.h"
#include "quiche/quic/core/quic_time.h"
#include "quiche/quic/core/quic_types.h"
#include "quiche/quic/moqt/moqt_error.h"
#include "quiche/quic/moqt/moqt_fetch_task.h"
#include "quiche/quic/moqt/moqt_framer.h"
#include "quiche/quic/moqt/moqt_key_value_pair.h"
#include "quiche/quic/moqt/moqt_messages.h"
#include "quiche/quic/moqt/moqt_names.h"
#include "quiche/quic/moqt/moqt_parser.h"
#include "quiche/quic/moqt/moqt_priority.h"
#include "quiche/quic/moqt/moqt_session_callbacks.h"
#include "quiche/quic/moqt/moqt_session_interface.h"
#include "quiche/quic/moqt/moqt_subscription.h"
#include "quiche/quic/moqt/moqt_trace_recorder.h"
#include "quiche/quic/moqt/moqt_track.h"
#include "quiche/quic/moqt/moqt_types.h"
#include "quiche/quic/moqt/test_tools/moqt_framer_utils.h"
#include "quiche/quic/moqt/test_tools/moqt_mock_visitor.h"
#include "quiche/quic/test_tools/mock_clock.h"
#include "quiche/quic/test_tools/quic_test_utils.h"
#include "quiche/common/platform/api/quiche_test.h"
#include "quiche/common/quiche_mem_slice.h"
#include "quiche/common/test_tools/quiche_test_utils.h"
#include "quiche/web_transport/test_tools/mock_web_transport.h"
#include "quiche/web_transport/web_transport.h"
namespace moqt::test {
class SubscriptionPublisherPeer {
public:
static const MessageParameters& parameters(
const SubscriptionPublisher& publisher) {
return publisher.parameters_;
}
};
class SubscribeRemoteTrackPeer {
public:
static const MessageParameters& parameters(
const SubscribeRemoteTrack& track) {
return track.const_parameters();
}
};
namespace {
using ::testing::_;
using ::testing::NotNull;
using ::testing::Return;
using ::testing::StrictMock;
class MockSessionToPublisherInterface : public SessionToPublisherInterface {
public:
~MockSessionToPublisherInterface() override = default;
MOCK_METHOD(bool, alternate_delivery_timeout, (), (const, override));
MOCK_METHOD(void, UpdateTrackPriority,
(uint64_t, std::optional<MoqtTrackPriority>, MoqtTrackPriority),
(override));
MOCK_METHOD(quic::QuicAlarmFactory*, alarm_factory, (), (override));
MOCK_METHOD(void, PublishIsDone, (uint64_t), (override));
MOCK_METHOD(webtransport::Session*, session, (), (override));
};
constexpr uint64_t kRequestId = 1;
constexpr uint64_t kTrackAlias = 10;
const FullTrackName kTrackName("foo", "bar");
class MoqtPublishPublisherStreamTest : public quiche::test::QuicheTest {
public:
MoqtPublishPublisherStreamTest()
: framer_(/*using_webtrans=*/true, quic::Perspective::IS_CLIENT),
message_parser_(kDefaultMoqtVersion, /*uses_web_transport=*/true,
quic::Perspective::IS_CLIENT),
track_publisher_(std::make_shared<TestTrackPublisher>(kTrackName)) {
// Construct the stream visitor.
stream_ = std::make_unique<MoqtPublishPublisherStream>(
&framer_, message_parser_, deleted_callback_.AsStdFunction(),
error_callback_.AsStdFunction(),
[this](std::variant<MessageParameters, MoqtRequestErrorInfo> response) {
response_ = response;
});
// Construct the SubscriptionPublisher.
parameters_.set_forward(true);
parameters_.delivery_timeout = quic::QuicTimeDelta::FromSeconds(1);
parameters_.group_order = MoqtDeliveryOrder::kAscending;
EXPECT_CALL(visitor_, session).WillRepeatedly(Return(&webtrans_));
auto publisher = std::make_unique<SubscriptionPublisher>(
framer_, track_publisher_, stream_.get(), kRequestId, kTrackAlias,
parameters_, &visitor_, /*monitoring_interface=*/nullptr, &mock_clock_,
trace_recorder_, /*is_publish=*/true);
publisher_ = publisher.get(); // Keep raw pointer for testing
stream_->SetPublisher(std::move(publisher));
EXPECT_CALL(mock_stream_, CanWrite).WillRepeatedly(Return(true));
}
MoqtFramer framer_;
MoqtControlMessageParser message_parser_;
webtransport::test::MockStream mock_stream_;
std::shared_ptr<TestTrackPublisher> track_publisher_;
testing::MockFunction<void(SubscriptionPublisher*)> deleted_callback_;
testing::StrictMock<testing::MockFunction<void(MoqtError, absl::string_view)>>
error_callback_;
MockSessionToPublisherInterface visitor_;
webtransport::test::MockSession webtrans_;
quic::MockClock mock_clock_;
MoqtTraceRecorder trace_recorder_;
MessageParameters parameters_;
std::unique_ptr<MoqtPublishPublisherStream> stream_;
SubscriptionPublisher* publisher_; // Raw pointer
std::optional<std::variant<MessageParameters, MoqtRequestErrorInfo>>
response_;
};
TEST_F(MoqtPublishPublisherStreamTest, OnStreamBoundSendsPublish) {
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kPublish), _))
.WillOnce(Return(absl::OkStatus()));
stream_->BindStream(&mock_stream_); // Calls OnStreamBound
}
TEST_F(MoqtPublishPublisherStreamTest, ReceiveRequestOk) {
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kPublish), _))
.WillOnce(Return(absl::OkStatus()));
stream_->BindStream(&mock_stream_); // Calls OnStreamBound
MoqtRequestOk request_ok;
request_ok.request_id = kRequestId;
request_ok.parameters.delivery_timeout = quic::QuicTimeDelta::FromSeconds(2);
request_ok.parameters.group_order = MoqtDeliveryOrder::kDescending;
QUICHE_EXPECT_OK(stream_->OnControlMessage(request_ok));
// Verify response callback was called.
ASSERT_TRUE(response_.has_value());
ASSERT_TRUE(std::holds_alternative<MessageParameters>(*response_));
MessageParameters resp_params = std::get<MessageParameters>(*response_);
EXPECT_EQ(resp_params.delivery_timeout,
request_ok.parameters.delivery_timeout);
EXPECT_EQ(resp_params.group_order, request_ok.parameters.group_order);
// Verify publisher parameters were updated.
const MessageParameters& pub_params =
SubscriptionPublisherPeer::parameters(*publisher_);
EXPECT_EQ(pub_params.delivery_timeout,
request_ok.parameters.delivery_timeout);
// Group order cannot be updated.
EXPECT_EQ(pub_params.group_order, parameters_.group_order);
}
TEST_F(MoqtPublishPublisherStreamTest, ReceiveRequestError) {
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kPublish), _))
.WillOnce(Return(absl::OkStatus()));
stream_->BindStream(&mock_stream_); // Calls OnStreamBound
MoqtRequestError request_error;
request_error.request_id = kRequestId;
request_error.error_code = RequestErrorCode::kUnauthorized;
request_error.retry_interval = quic::QuicTimeDelta::FromSeconds(5);
request_error.reason_phrase = "Unauthorized";
QUICHE_EXPECT_OK(stream_->OnControlMessage(request_error));
// Verify response callback was called with error.
ASSERT_TRUE(response_.has_value());
ASSERT_TRUE(std::holds_alternative<MoqtRequestErrorInfo>(*response_));
MoqtRequestErrorInfo resp_error = std::get<MoqtRequestErrorInfo>(*response_);
EXPECT_EQ(resp_error.error_code, request_error.error_code);
EXPECT_EQ(resp_error.retry_interval, request_error.retry_interval);
EXPECT_EQ(resp_error.reason_phrase, request_error.reason_phrase);
}
TEST_F(MoqtPublishPublisherStreamTest, ReceiveRequestUpdate) {
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kPublish), _))
.WillOnce(Return(absl::OkStatus()));
stream_->BindStream(&mock_stream_);
QUICHE_EXPECT_OK(stream_->OnControlMessage(MoqtRequestOk{kRequestId}));
// Set largest location on publisher
track_publisher_->AddObject(Location(1, 2), 0, "payload", true);
MoqtRequestUpdate request_update;
request_update.request_id = kRequestId + 2;
request_update.existing_request_id = kRequestId;
request_update.parameters.delivery_timeout =
quic::QuicTimeDelta::FromSeconds(3);
request_update.parameters.subscriber_priority = 5;
request_update.parameters.subscription_filter.emplace(
MoqtFilterType::kLargestObject);
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestOk), _))
.WillOnce(Return(absl::OkStatus()));
QUICHE_EXPECT_OK(stream_->OnControlMessage(request_update));
// Verify publisher parameters were updated.
const MessageParameters& pub_params =
SubscriptionPublisherPeer::parameters(*publisher_);
EXPECT_EQ(pub_params.delivery_timeout,
request_update.parameters.delivery_timeout);
EXPECT_EQ(pub_params.subscriber_priority,
request_update.parameters.subscriber_priority);
// Verify filter was updated based on largest location (1, 2) -> (1, 3)
// AbsoluteStart
ASSERT_TRUE(pub_params.subscription_filter.has_value());
EXPECT_EQ(pub_params.subscription_filter->type(),
MoqtFilterType::kAbsoluteStart);
EXPECT_EQ(pub_params.subscription_filter->start(), Location(1, 3));
}
class MoqtPublishSubscriberStreamTest : public quiche::test::QuicheTest {
public:
MoqtPublishSubscriberStreamTest()
: framer_(/*using_webtrans=*/true, quic::Perspective::IS_SERVER),
message_parser_(kDefaultMoqtVersion, /*uses_web_transport=*/true,
quic::Perspective::IS_SERVER),
incoming_publish_callback_(
incoming_publish_callback_mock_.AsStdFunction()) {
stream_ = std::make_unique<MoqtPublishSubscriberStream>(
&framer_, message_parser_, &mock_clock_, &mock_alarm_factory_,
error_callback_.AsStdFunction(), &incoming_publish_callback_,
mock_add_callback_.AsStdFunction(),
mock_remove_callback_.AsStdFunction());
stream_->BindStream(&mock_stream_);
EXPECT_CALL(mock_stream_, CanWrite).WillRepeatedly(Return(true));
}
MoqtPublish DefaultPublish() {
return MoqtPublish{kRequestId, kTrackName, kTrackAlias, MessageParameters(),
TrackExtensions()};
}
MoqtFramer framer_;
MoqtControlMessageParser message_parser_;
quic::MockClock mock_clock_;
quic::test::MockAlarmFactory mock_alarm_factory_;
testing::StrictMock<testing::MockFunction<void(MoqtError, absl::string_view)>>
error_callback_;
testing::MockFunction<SubscribeVisitor*(
const FullTrackName&, const MessageParameters&, const TrackExtensions&,
MoqtResponseCallback)>
incoming_publish_callback_mock_;
MoqtIncomingPublishCallback incoming_publish_callback_;
testing::MockFunction<bool(SubscribeRemoteTrack*)> mock_add_callback_;
testing::MockFunction<void(SubscribeRemoteTrack*)> mock_remove_callback_;
StrictMock<MockSubscribeRemoteTrackVisitor> mock_subscribe_visitor_;
MoqtResponseCallback captured_response_callback_;
webtransport::test::MockStream mock_stream_;
std::unique_ptr<MoqtPublishSubscriberStream> stream_;
};
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishAndAccept) {
EXPECT_CALL(mock_subscribe_visitor_, OnReply(kTrackName, _))
.WillOnce(
[](const FullTrackName&,
const std::variant<SubscribeOkData, MoqtRequestErrorInfo>& reply) {
EXPECT_TRUE(std::holds_alternative<SubscribeOkData>(reply));
});
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce([this](const FullTrackName&, const MessageParameters&,
const TrackExtensions&, MoqtResponseCallback callback) {
captured_response_callback_ = std::move(callback);
return &mock_subscribe_visitor_;
});
SubscribeRemoteTrack* captured_subscriber = nullptr;
EXPECT_CALL(mock_add_callback_, Call(NotNull()))
.WillOnce([&](SubscribeRemoteTrack* subscriber) {
captured_subscriber = subscriber;
return true;
});
MoqtPublish publish = DefaultPublish();
publish.parameters.delivery_timeout = quic::QuicTimeDelta::FromSeconds(1);
QUICHE_EXPECT_OK(stream_->OnControlMessage(publish));
// Verify subscriber is created.
ASSERT_NE(captured_subscriber, nullptr);
EXPECT_EQ(captured_subscriber->track_alias(), kTrackAlias);
EXPECT_EQ(captured_subscriber->visitor(), &mock_subscribe_visitor_);
// Verify REQUEST_OK response was sent.
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestOk), _))
.WillOnce(Return(absl::OkStatus()));
MessageParameters response_parameters;
response_parameters.delivery_timeout = quic::QuicTimeDelta::FromSeconds(2);
std::move(captured_response_callback_)(response_parameters);
// Verify subscriber parameters were updated.
const MessageParameters& sub_params =
SubscribeRemoteTrackPeer::parameters(*captured_subscriber);
EXPECT_EQ(sub_params.delivery_timeout, response_parameters.delivery_timeout);
EXPECT_CALL(mock_subscribe_visitor_, OnPublishDone);
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishAndReject) {
MoqtPublish publish = DefaultPublish();
// Callback returns nullptr (rejection).
EXPECT_CALL(mock_add_callback_, Call(NotNull())).WillOnce(Return(true));
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(nullptr));
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestError), _))
.WillOnce(Return(absl::OkStatus()));
EXPECT_CALL(mock_remove_callback_, Call);
QUICHE_EXPECT_OK(stream_->OnControlMessage(publish));
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceiveTwoPublishOnStream) {
MoqtPublish publish = DefaultPublish();
EXPECT_CALL(mock_add_callback_, Call(NotNull())).WillOnce(Return(true));
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(&mock_subscribe_visitor_));
EXPECT_CALL(mock_subscribe_visitor_, OnReply(kTrackName, _))
.WillOnce(
[](const FullTrackName&,
const std::variant<SubscribeOkData, MoqtRequestErrorInfo>& reply) {
EXPECT_TRUE(std::holds_alternative<SubscribeOkData>(reply));
});
QUICHE_EXPECT_OK(stream_->OnControlMessage(publish));
// Receive second PUBLISH on same stream.
publish.request_id = kRequestId + 2;
publish.full_track_name = FullTrackName("dead", "beef");
publish.track_alias = kTrackAlias + 1;
EXPECT_FALSE(stream_->OnControlMessage(publish).ok());
EXPECT_CALL(mock_subscribe_visitor_, OnPublishDone(kTrackName));
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishDuplicate) {
MoqtPublish publish = DefaultPublish();
publish.request_id = kRequestId + 2;
publish.full_track_name = kTrackName;
publish.track_alias = kTrackAlias;
EXPECT_CALL(mock_add_callback_, Call).WillOnce(Return(false));
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestError), _))
.WillOnce(Return(absl::OkStatus()));
EXPECT_CALL(mock_remove_callback_, Call);
QUICHE_EXPECT_OK(stream_->OnControlMessage(publish));
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceiveRequestUpdate) {
MoqtPublish publish = DefaultPublish();
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(&mock_subscribe_visitor_));
SubscribeRemoteTrack* captured_subscriber = nullptr;
EXPECT_CALL(mock_add_callback_, Call(NotNull()))
.WillOnce([&](SubscribeRemoteTrack* track) {
captured_subscriber = track;
return true;
});
EXPECT_CALL(mock_subscribe_visitor_, OnReply(kTrackName, _))
.WillOnce(
[](const FullTrackName&,
const std::variant<SubscribeOkData, MoqtRequestErrorInfo>& reply) {
EXPECT_TRUE(std::holds_alternative<SubscribeOkData>(reply));
});
QUICHE_EXPECT_OK(stream_->OnControlMessage(publish));
// Now receive REQUEST_UPDATE.
MoqtRequestUpdate request_update;
request_update.request_id = kRequestId + 2;
request_update.existing_request_id = kRequestId;
request_update.parameters.delivery_timeout =
quic::QuicTimeDelta::FromSeconds(3);
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestOk), _))
.WillOnce(Return(absl::OkStatus()));
QUICHE_EXPECT_OK(stream_->OnControlMessage(request_update));
// Verify subscriber parameters were updated.
ASSERT_NE(captured_subscriber, nullptr);
const MessageParameters& sub_params =
SubscribeRemoteTrackPeer::parameters(*captured_subscriber);
EXPECT_EQ(sub_params.delivery_timeout,
request_update.parameters.delivery_timeout);
EXPECT_CALL(mock_subscribe_visitor_, OnPublishDone(kTrackName));
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishDone) {
MoqtPublish publish = DefaultPublish();
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(&mock_subscribe_visitor_));
EXPECT_CALL(mock_add_callback_, Call(NotNull())).WillOnce(Return(true));
EXPECT_CALL(mock_subscribe_visitor_, OnReply(kTrackName, _))
.WillOnce(
[](const FullTrackName&,
const std::variant<SubscribeOkData, MoqtRequestErrorInfo>& reply) {
EXPECT_TRUE(std::holds_alternative<SubscribeOkData>(reply));
});
QUICHE_EXPECT_OK(stream_->OnControlMessage(publish));
// Now receive PUBLISH_DONE.
MoqtPublishDone publish_done;
publish_done.request_id = kRequestId;
publish_done.status_code = PublishDoneCode::kTrackEnded;
publish_done.stream_count = 0; // Trigger immediate Destroy
EXPECT_CALL(mock_stream_, Writev)
.WillOnce([](absl::Span<quiche::QuicheMemSlice> data,
const webtransport::StreamWriteOptions& options) {
EXPECT_TRUE(data.empty());
EXPECT_TRUE(options.send_fin());
return absl::OkStatus();
});
EXPECT_CALL(mock_remove_callback_, Call);
EXPECT_CALL(mock_subscribe_visitor_, OnPublishDone(kTrackName));
QUICHE_EXPECT_OK(stream_->OnControlMessage(publish_done));
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishAndRejectCallback) {
MoqtPublish publish = DefaultPublish();
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce([this](const FullTrackName&, const MessageParameters&,
const TrackExtensions&, MoqtResponseCallback callback) {
captured_response_callback_ = std::move(callback);
return &mock_subscribe_visitor_;
});
EXPECT_CALL(mock_add_callback_, Call(NotNull())).WillOnce(Return(true));
EXPECT_CALL(mock_subscribe_visitor_, OnReply(kTrackName, _))
.WillOnce(
[](const FullTrackName&,
const std::variant<SubscribeOkData, MoqtRequestErrorInfo>& reply) {
EXPECT_TRUE(std::holds_alternative<SubscribeOkData>(reply));
});
QUICHE_EXPECT_OK(stream_->OnControlMessage(publish));
// Now call the response callback with error (reject).
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestError), _))
.WillOnce(Return(absl::OkStatus()));
MoqtRequestErrorInfo error_info{RequestErrorCode::kUninterested, std::nullopt,
"rejected by app"};
EXPECT_CALL(mock_remove_callback_, Call);
EXPECT_CALL(mock_subscribe_visitor_, OnPublishDone(kTrackName));
std::move(captured_response_callback_)(error_info);
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishDoneOnRejectedStream) {
MoqtPublish publish = DefaultPublish();
// Callback returns nullptr (rejection).
EXPECT_CALL(mock_add_callback_, Call(NotNull())).WillOnce(Return(true));
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(nullptr));
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestError), _))
.WillOnce(Return(absl::OkStatus()));
EXPECT_CALL(mock_remove_callback_, Call);
QUICHE_EXPECT_OK(stream_->OnControlMessage(publish));
// Now receive PUBLISH_DONE.
MoqtPublishDone publish_done;
publish_done.request_id = kRequestId;
publish_done.status_code = PublishDoneCode::kTrackEnded;
publish_done.stream_count = 0;
QUICHE_EXPECT_OK(stream_->OnControlMessage(publish_done));
}
TEST_F(MoqtPublishSubscriberStreamTest, DuplicatePublishOnSameStream) {
MoqtPublish publish = DefaultPublish();
EXPECT_CALL(mock_add_callback_, Call(NotNull())).WillOnce(Return(true));
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(&mock_subscribe_visitor_));
EXPECT_CALL(mock_subscribe_visitor_, OnReply(kTrackName, _))
.WillOnce(
[](const FullTrackName&,
const std::variant<SubscribeOkData, MoqtRequestErrorInfo>& reply) {
EXPECT_TRUE(std::holds_alternative<SubscribeOkData>(reply));
});
QUICHE_EXPECT_OK(stream_->OnControlMessage(publish));
// Receive second PUBLISH on same stream. Should cause a session error.
EXPECT_CALL(error_callback_, Call(MoqtError::kProtocolViolation,
"Multiple PUBLISH on the same stream"));
MoqtPublish publish2 = DefaultPublish();
publish2.request_id = kRequestId + 2;
publish2.full_track_name = FullTrackName("dead", "beef");
publish2.track_alias = kTrackAlias + 1;
stream_->CheckStatus(stream_->OnControlMessage(publish2));
EXPECT_CALL(mock_subscribe_visitor_, OnPublishDone(kTrackName));
}
TEST_F(MoqtPublishSubscriberStreamTest, DuplicatePublishOnDifferentStreams) {
MoqtPublish publish1 = DefaultPublish();
EXPECT_CALL(mock_add_callback_, Call(NotNull())).WillOnce(Return(true));
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(&mock_subscribe_visitor_));
EXPECT_CALL(mock_subscribe_visitor_, OnReply(kTrackName, _))
.WillOnce(
[](const FullTrackName&,
const std::variant<SubscribeOkData, MoqtRequestErrorInfo>& reply) {
EXPECT_TRUE(std::holds_alternative<SubscribeOkData>(reply));
});
QUICHE_EXPECT_OK(stream_->OnControlMessage(publish1));
// Second stream
testing::MockFunction<void(MoqtError, absl::string_view)> error_callback2;
testing::MockFunction<bool(SubscribeRemoteTrack*)> mock_add_callback2;
testing::MockFunction<void(SubscribeRemoteTrack*)> mock_remove_callback2;
testing::MockFunction<SubscribeVisitor*(
const FullTrackName&, const MessageParameters&, const TrackExtensions&,
MoqtResponseCallback)>
incoming_publish_callback_mock2;
MoqtIncomingPublishCallback incoming_publish_callback2 =
incoming_publish_callback_mock2.AsStdFunction();
webtransport::test::MockStream mock_stream2;
MoqtPublishSubscriberStream stream2(
&framer_, message_parser_, &mock_clock_, &mock_alarm_factory_,
error_callback2.AsStdFunction(), &incoming_publish_callback2,
mock_add_callback2.AsStdFunction(),
mock_remove_callback2.AsStdFunction());
stream2.BindStream(&mock_stream2);
EXPECT_CALL(mock_stream2, CanWrite).WillRepeatedly(Return(true));
// Duplicate PUBLISH (same track name) on stream2 is rejected.
MoqtPublish publish2 = DefaultPublish();
publish2.request_id = kRequestId + 2;
publish2.full_track_name = kTrackName;
EXPECT_CALL(mock_add_callback2, Call(NotNull())).WillOnce(Return(false));
EXPECT_CALL(mock_stream2,
Writev(ControlMessageOfType(MoqtMessageType::kRequestError), _))
.WillOnce(Return(absl::OkStatus()));
EXPECT_CALL(mock_remove_callback2, Call);
QUICHE_EXPECT_OK(stream2.OnControlMessage(publish2));
// Finally, a PUBLISH on the second stream triggers a session error.
EXPECT_CALL(error_callback2, Call(MoqtError::kProtocolViolation,
"Multiple PUBLISH on the same stream"));
MoqtPublish publish3 = DefaultPublish();
publish3.request_id = kRequestId + 4;
publish3.full_track_name = kTrackName;
stream2.CheckStatus(stream2.OnControlMessage(publish3));
// Test teardown expectations
EXPECT_CALL(mock_subscribe_visitor_, OnPublishDone(kTrackName));
}
} // namespace
} // namespace moqt::test