blob: 6c863a39d49fb8da6eaaccf97019b138531c5d52 [file]
// 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/status/statusor.h"
#include "absl/strings/string_view.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_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/web_transport/test_tools/in_memory_stream.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));
};
class BidiStreamWithReset
: public webtransport::test::InMemoryStreamWithWriteBuffer {
public:
using InMemoryStreamWithWriteBuffer::InMemoryStreamWithWriteBuffer;
void ResetWithUserCode(webtransport::StreamErrorCode error) override {
last_reset_code_ = error;
}
std::optional<webtransport::StreamErrorCode> last_reset_code() const {
return last_reset_code_;
}
private:
std::optional<webtransport::StreamErrorCode> last_reset_code_;
};
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_visitor_ = 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_visitor_.get(), kRequestId,
kTrackAlias, parameters_, &visitor_, /*monitoring_interface=*/nullptr,
&mock_clock_, trace_recorder_, /*is_publish=*/true);
publisher_ = publisher.get(); // Keep raw pointer for testing
stream_visitor_->SetPublisher(std::move(publisher));
}
MoqtFramer framer_;
MoqtControlMessageParser message_parser_;
std::shared_ptr<TestTrackPublisher> track_publisher_;
testing::MockFunction<void()> 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_visitor_;
SubscriptionPublisher* publisher_; // Raw pointer
std::optional<std::variant<MessageParameters, MoqtRequestErrorInfo>>
response_;
};
TEST_F(MoqtPublishPublisherStreamTest, OnStreamBoundSendsPublish) {
webtransport::test::InMemoryStreamWithWriteBuffer stream(0);
stream_visitor_->BindStream(&stream); // Calls OnStreamBound
// Verify PUBLISH message was sent.
std::string& written = stream.write_buffer();
MoqtControlStreamParser parser(&stream);
// Feed the written data back to a parser to verify it.
webtransport::test::InMemoryStream read_stream(0);
read_stream.Receive(written);
MoqtControlStreamParser read_parser(&read_stream);
absl::StatusOr<MoqtRawControlMessage> message = read_parser.ReadNextMessage();
ASSERT_TRUE(message.ok());
EXPECT_EQ(message->type, MoqtMessageType::kPublish);
MoqtControlMessageParser cmp(kDefaultMoqtVersion, true,
quic::Perspective::IS_CLIENT);
absl::StatusOr<MoqtPublish> publish = cmp.ProcessPublish(message->payload);
ASSERT_TRUE(publish.ok());
EXPECT_EQ(publish->request_id, kRequestId);
EXPECT_EQ(publish->full_track_name, kTrackName);
EXPECT_EQ(publish->track_alias, kTrackAlias);
EXPECT_EQ(publish->parameters.delivery_timeout, parameters_.delivery_timeout);
EXPECT_EQ(publish->parameters.group_order, parameters_.group_order);
}
TEST_F(MoqtPublishPublisherStreamTest, ReceiveRequestOk) {
webtransport::test::InMemoryStreamWithWriteBuffer stream(0);
stream_visitor_->BindStream(&stream);
MoqtRequestOk request_ok;
request_ok.request_id = kRequestId;
request_ok.parameters.delivery_timeout = quic::QuicTimeDelta::FromSeconds(2);
request_ok.parameters.group_order = MoqtDeliveryOrder::kDescending;
stream.Receive(framer_.SerializeRequestOk(request_ok).AsStringView());
stream_visitor_->OnCanRead();
// 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) {
webtransport::test::InMemoryStreamWithWriteBuffer stream(0);
stream_visitor_->BindStream(&stream);
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";
stream.Receive(framer_.SerializeRequestError(request_error).AsStringView());
stream_visitor_->OnCanRead();
// 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) {
webtransport::test::InMemoryStreamWithWriteBuffer stream(0);
stream_visitor_->BindStream(&stream);
stream.write_buffer().clear(); // Clear initial PUBLISH
// 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);
stream.Receive(framer_.SerializeRequestUpdate(request_update).AsStringView());
stream_visitor_->OnCanRead();
// 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));
// Verify REQUEST_OK response was sent.
std::string& written = stream.write_buffer();
webtransport::test::InMemoryStream read_stream(0);
read_stream.Receive(written);
MoqtControlStreamParser read_parser(&read_stream);
absl::StatusOr<MoqtRawControlMessage> message = read_parser.ReadNextMessage();
ASSERT_TRUE(message.ok());
EXPECT_EQ(message->type, MoqtMessageType::kRequestOk);
MoqtControlMessageParser cmp(kDefaultMoqtVersion, true,
quic::Perspective::IS_CLIENT);
absl::StatusOr<MoqtRequestOk> request_ok =
cmp.ProcessRequestOk(message->payload);
ASSERT_TRUE(request_ok.ok());
EXPECT_EQ(request_ok->request_id, request_update.request_id);
}
TEST_F(MoqtPublishPublisherStreamTest, ReceiveRequestOkMismatchedId) {
BidiStreamWithReset stream(0);
stream_visitor_->BindStream(&stream);
// Receive REQUEST_OK with mismatched ID.
MoqtRequestOk request_ok;
request_ok.request_id = kRequestId + 1; // Mismatched
EXPECT_CALL(error_callback_,
Call(MoqtError::kProtocolViolation,
"REQUEST_OK does not match PUBLISH request ID"));
stream.Receive(framer_.SerializeRequestOk(request_ok).AsStringView());
stream_visitor_->OnCanRead();
}
TEST_F(MoqtPublishPublisherStreamTest, ReceiveRequestErrorMismatchedId) {
BidiStreamWithReset stream(0);
stream_visitor_->BindStream(&stream);
// Receive REQUEST_ERROR with mismatched ID.
MoqtRequestError request_error;
request_error.request_id = kRequestId + 1; // Mismatched
request_error.error_code = RequestErrorCode::kUninterested;
EXPECT_CALL(error_callback_,
Call(MoqtError::kProtocolViolation,
"REQUEST_OK does not match PUBLISH request ID"));
stream.Receive(framer_.SerializeRequestError(request_error).AsStringView());
stream_visitor_->OnCanRead();
}
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()) {
SubscribeRemoteTrack::SubscribeCallbacks callbacks;
callbacks.query_name = [this](const FullTrackName& name) {
return query_name_mock_.Call(name);
};
callbacks.register_name = [this](const FullTrackName& name,
SubscribeRemoteTrack* track) {
register_name_mock_.Call(name, track);
};
callbacks.register_alias = [this](uint64_t alias,
SubscribeRemoteTrack* track) {
return register_alias_mock_.Call(alias, track);
};
callbacks.unregister = [this](const FullTrackName& name,
std::optional<uint64_t> alias) {
unregister_mock_.Call(name, alias);
};
stream_visitor_ = std::make_unique<MoqtPublishSubscriberStream>(
&framer_, message_parser_, &mock_clock_, &mock_alarm_factory_,
error_callback_.AsStdFunction(), &incoming_publish_callback_,
std::move(callbacks));
}
void ExpectSubscriberDestruction() {
EXPECT_CALL(unregister_mock_,
Call(kTrackName, std::optional<uint64_t>(kTrackAlias)))
.Times(1);
}
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<SubscribeRemoteTrack*(const FullTrackName&)>
query_name_mock_;
testing::MockFunction<void(const FullTrackName&, SubscribeRemoteTrack*)>
register_name_mock_;
testing::MockFunction<bool(uint64_t, SubscribeRemoteTrack*)>
register_alias_mock_;
testing::MockFunction<void(const FullTrackName&, std::optional<uint64_t>)>
unregister_mock_;
StrictMock<MockSubscribeRemoteTrackVisitor> mock_subscribe_visitor_;
MoqtResponseCallback captured_response_callback_;
std::unique_ptr<MoqtPublishSubscriberStream> stream_visitor_;
};
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishAndAccept) {
BidiStreamWithReset stream(0);
stream_visitor_->BindStream(&stream);
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(mock_subscribe_visitor_, OnPublishDone(kTrackName));
ExpectSubscriberDestruction();
MoqtPublish publish;
publish.request_id = kRequestId;
publish.full_track_name = kTrackName;
publish.track_alias = kTrackAlias;
publish.parameters.delivery_timeout = quic::QuicTimeDelta::FromSeconds(1);
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(query_name_mock_, Call(kTrackName)).WillOnce(Return(nullptr));
EXPECT_CALL(register_name_mock_, Call(kTrackName, NotNull())).Times(1);
SubscribeRemoteTrack* captured_subscriber = nullptr;
EXPECT_CALL(register_alias_mock_, Call(kTrackAlias, NotNull()))
.WillOnce([&](uint64_t, SubscribeRemoteTrack* track) {
captured_subscriber = track;
return true;
});
stream.Receive(framer_.SerializePublish(publish).AsStringView());
stream_visitor_->OnCanRead();
// Verify subscriber is created.
ASSERT_NE(captured_subscriber, nullptr);
EXPECT_EQ(captured_subscriber->track_alias(), kTrackAlias);
EXPECT_EQ(captured_subscriber->visitor(), &mock_subscribe_visitor_);
// Now call the response callback with success.
stream.write_buffer().clear();
MessageParameters response_parameters;
response_parameters.delivery_timeout = quic::QuicTimeDelta::FromSeconds(2);
std::move(captured_response_callback_)(response_parameters);
// Verify REQUEST_OK response was sent.
std::string& written = stream.write_buffer();
webtransport::test::InMemoryStream read_stream(0);
read_stream.Receive(written);
MoqtControlStreamParser read_parser(&read_stream);
absl::StatusOr<MoqtRawControlMessage> message = read_parser.ReadNextMessage();
ASSERT_TRUE(message.ok());
EXPECT_EQ(message->type, MoqtMessageType::kRequestOk);
MoqtControlMessageParser cmp(kDefaultMoqtVersion, true,
quic::Perspective::IS_SERVER);
absl::StatusOr<MoqtRequestOk> request_ok =
cmp.ProcessRequestOk(message->payload);
ASSERT_TRUE(request_ok.ok());
EXPECT_EQ(request_ok->request_id, kRequestId);
EXPECT_EQ(request_ok->parameters.delivery_timeout,
response_parameters.delivery_timeout);
// Verify subscriber parameters were updated.
const MessageParameters& sub_params =
SubscribeRemoteTrackPeer::parameters(*captured_subscriber);
EXPECT_EQ(sub_params.delivery_timeout, response_parameters.delivery_timeout);
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishAndReject) {
BidiStreamWithReset stream(0);
stream_visitor_->BindStream(&stream);
MoqtPublish publish;
publish.request_id = kRequestId;
publish.full_track_name = kTrackName;
publish.track_alias = kTrackAlias;
// Callback returns nullptr (rejection).
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(nullptr));
stream.Receive(framer_.SerializePublish(publish).AsStringView());
stream_visitor_->OnCanRead();
// Verify REQUEST_ERROR was sent.
std::string& written = stream.write_buffer();
webtransport::test::InMemoryStream read_stream(0);
read_stream.Receive(written);
MoqtControlStreamParser read_parser(&read_stream);
absl::StatusOr<MoqtRawControlMessage> message = read_parser.ReadNextMessage();
ASSERT_TRUE(message.ok());
EXPECT_EQ(message->type, MoqtMessageType::kRequestError);
MoqtControlMessageParser cmp(kDefaultMoqtVersion, true,
quic::Perspective::IS_SERVER);
absl::StatusOr<MoqtRequestError> request_error =
cmp.ProcessRequestError(message->payload);
ASSERT_TRUE(request_error.ok());
EXPECT_EQ(request_error->request_id, kRequestId);
EXPECT_EQ(request_error->error_code, RequestErrorCode::kUninterested);
EXPECT_TRUE(stream.fin_sent());
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishDuplicate) {
BidiStreamWithReset stream(0);
stream_visitor_->BindStream(&stream);
MoqtPublish publish;
publish.request_id = kRequestId;
publish.full_track_name = kTrackName;
publish.track_alias = kTrackAlias;
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(&mock_subscribe_visitor_));
EXPECT_CALL(query_name_mock_, Call(kTrackName)).WillOnce(Return(nullptr));
EXPECT_CALL(register_name_mock_, Call(kTrackName, NotNull())).Times(1);
EXPECT_CALL(register_alias_mock_, Call(kTrackAlias, 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));
});
EXPECT_CALL(mock_subscribe_visitor_, OnPublishDone(kTrackName));
ExpectSubscriberDestruction();
stream.Receive(framer_.SerializePublish(publish).AsStringView());
stream_visitor_->OnCanRead();
// Send second PUBLISH on same stream.
stream.Receive(framer_.SerializePublish(publish).AsStringView());
// It should return InvalidArgumentError, which calls OnFatalError in
// MoqtBidiStreamBase.
EXPECT_CALL(error_callback_, Call(MoqtError::kProtocolViolation,
"Multiple PUBLISH on the same stream"));
stream_visitor_->OnCanRead();
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishDuplicateName) {
BidiStreamWithReset stream(0);
stream_visitor_->BindStream(&stream);
MoqtPublish publish;
publish.request_id = kRequestId + 2;
publish.full_track_name = kTrackName;
publish.track_alias = kTrackAlias;
// Simulate an existing established track.
MoqtSubscribe sub;
sub.full_track_name = kTrackName;
sub.request_id = kRequestId;
StrictMock<MockSubscribeRemoteTrackVisitor> existing_visitor;
SubscribeRemoteTrack existing_track(sub, &existing_visitor, []() {}, {});
existing_track.OnObjectOrOk();
EXPECT_CALL(existing_visitor, OnPublishDone(kTrackName)).Times(1);
EXPECT_CALL(query_name_mock_, Call(kTrackName))
.WillOnce(Return(&existing_track));
stream.Receive(framer_.SerializePublish(publish).AsStringView());
stream_visitor_->OnCanRead();
// Verify REQUEST_ERROR was sent.
std::string& written = stream.write_buffer();
webtransport::test::InMemoryStream read_stream(0);
read_stream.Receive(written);
MoqtControlStreamParser read_parser(&read_stream);
absl::StatusOr<MoqtRawControlMessage> message = read_parser.ReadNextMessage();
ASSERT_TRUE(message.ok());
EXPECT_EQ(message->type, MoqtMessageType::kRequestError);
MoqtControlMessageParser cmp(kDefaultMoqtVersion, true,
quic::Perspective::IS_SERVER);
absl::StatusOr<MoqtRequestError> request_error =
cmp.ProcessRequestError(message->payload);
ASSERT_TRUE(request_error.ok());
EXPECT_EQ(request_error->request_id, publish.request_id);
EXPECT_EQ(request_error->error_code,
RequestErrorCode::kDuplicateSubscription);
EXPECT_TRUE(stream.fin_sent());
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishDuplicateAlias) {
BidiStreamWithReset stream(0);
stream_visitor_->BindStream(&stream);
MoqtPublish publish;
publish.request_id = kRequestId;
publish.full_track_name = kTrackName;
publish.track_alias = kTrackAlias;
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(&mock_subscribe_visitor_));
EXPECT_CALL(query_name_mock_, Call(kTrackName)).WillOnce(Return(nullptr));
EXPECT_CALL(register_name_mock_, Call(kTrackName, NotNull())).Times(1);
// Return duplicate alias error (false) from alias callback.
EXPECT_CALL(register_alias_mock_, Call(kTrackAlias, NotNull()))
.WillOnce(Return(false));
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(mock_subscribe_visitor_, OnPublishDone(kTrackName));
ExpectSubscriberDestruction();
// It should call OnFatalError, which calls error_callback_.
// Note: The error message is now empty because we pass
// AlreadyExistsError("").
EXPECT_CALL(error_callback_, Call(MoqtError::kDuplicateTrackAlias, ""));
stream.Receive(framer_.SerializePublish(publish).AsStringView());
stream_visitor_->OnCanRead();
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceiveRequestUpdate) {
// First, establish subscription.
BidiStreamWithReset stream(0);
stream_visitor_->BindStream(&stream);
MoqtPublish publish;
publish.request_id = kRequestId;
publish.full_track_name = kTrackName;
publish.track_alias = kTrackAlias;
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(&mock_subscribe_visitor_));
EXPECT_CALL(query_name_mock_, Call(kTrackName)).WillOnce(Return(nullptr));
EXPECT_CALL(register_name_mock_, Call(kTrackName, NotNull())).Times(1);
SubscribeRemoteTrack* captured_subscriber = nullptr;
EXPECT_CALL(register_alias_mock_, Call(kTrackAlias, NotNull()))
.WillOnce([&](uint64_t, 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));
});
EXPECT_CALL(mock_subscribe_visitor_, OnPublishDone(kTrackName));
ExpectSubscriberDestruction();
stream.Receive(framer_.SerializePublish(publish).AsStringView());
stream_visitor_->OnCanRead();
stream.write_buffer().clear();
// 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);
stream.Receive(framer_.SerializeRequestUpdate(request_update).AsStringView());
stream_visitor_->OnCanRead();
// 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);
// Verify REQUEST_OK response was sent.
std::string& written = stream.write_buffer();
webtransport::test::InMemoryStream read_stream(0);
read_stream.Receive(written);
MoqtControlStreamParser read_parser(&read_stream);
absl::StatusOr<MoqtRawControlMessage> message = read_parser.ReadNextMessage();
ASSERT_TRUE(message.ok());
EXPECT_EQ(message->type, MoqtMessageType::kRequestOk);
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishDone) {
// First, establish subscription.
BidiStreamWithReset stream(0);
stream_visitor_->BindStream(&stream);
MoqtPublish publish;
publish.request_id = kRequestId;
publish.full_track_name = kTrackName;
publish.track_alias = kTrackAlias;
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(&mock_subscribe_visitor_));
EXPECT_CALL(query_name_mock_, Call(kTrackName)).WillOnce(Return(nullptr));
EXPECT_CALL(register_name_mock_, Call(kTrackName, NotNull())).Times(1);
EXPECT_CALL(register_alias_mock_, Call(kTrackAlias, 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));
});
EXPECT_CALL(mock_subscribe_visitor_, OnPublishDone(kTrackName));
ExpectSubscriberDestruction();
stream.Receive(framer_.SerializePublish(publish).AsStringView());
stream_visitor_->OnCanRead();
// 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
stream.Receive(framer_.SerializePublishDone(publish_done).AsStringView());
stream_visitor_->OnCanRead();
EXPECT_EQ(stream.last_reset_code(), kResetCodeCancelled);
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishAndRejectCallback) {
BidiStreamWithReset stream(0);
stream_visitor_->BindStream(&stream);
MoqtPublish publish;
publish.request_id = kRequestId;
publish.full_track_name = kTrackName;
publish.track_alias = kTrackAlias;
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(query_name_mock_, Call(kTrackName)).WillOnce(Return(nullptr));
EXPECT_CALL(register_name_mock_, Call(kTrackName, NotNull())).Times(1);
EXPECT_CALL(register_alias_mock_, Call(kTrackAlias, 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));
});
EXPECT_CALL(mock_subscribe_visitor_, OnPublishDone(kTrackName));
ExpectSubscriberDestruction();
stream.Receive(framer_.SerializePublish(publish).AsStringView());
stream_visitor_->OnCanRead();
// Now call the response callback with error (reject).
stream.write_buffer().clear();
MoqtRequestErrorInfo error_info{RequestErrorCode::kUninterested, std::nullopt,
"rejected by app"};
std::move(captured_response_callback_)(error_info);
// Verify REQUEST_ERROR response was sent.
std::string& written = stream.write_buffer();
webtransport::test::InMemoryStream read_stream(0);
read_stream.Receive(written);
MoqtControlStreamParser read_parser(&read_stream);
absl::StatusOr<MoqtRawControlMessage> message = read_parser.ReadNextMessage();
ASSERT_TRUE(message.ok());
EXPECT_EQ(message->type, MoqtMessageType::kRequestError);
MoqtControlMessageParser cmp(kDefaultMoqtVersion, true,
quic::Perspective::IS_SERVER);
absl::StatusOr<MoqtRequestError> request_error =
cmp.ProcessRequestError(message->payload);
ASSERT_TRUE(request_error.ok());
EXPECT_EQ(request_error->request_id, kRequestId);
EXPECT_EQ(request_error->error_code, RequestErrorCode::kUninterested);
EXPECT_EQ(request_error->reason_phrase, "rejected by app");
}
TEST_F(MoqtPublishSubscriberStreamTest, ReceivePublishDoneOnRejectedStream) {
BidiStreamWithReset stream(0);
stream_visitor_->BindStream(&stream);
MoqtPublish publish;
publish.request_id = kRequestId;
publish.full_track_name = kTrackName;
publish.track_alias = kTrackAlias;
// Callback returns nullptr (rejection).
EXPECT_CALL(incoming_publish_callback_mock_, Call(kTrackName, _, _, _))
.WillOnce(Return(nullptr));
stream.Receive(framer_.SerializePublish(publish).AsStringView());
stream_visitor_->OnCanRead();
// Now receive PUBLISH_DONE.
MoqtPublishDone publish_done;
publish_done.request_id = kRequestId;
publish_done.status_code = PublishDoneCode::kTrackEnded;
publish_done.stream_count = 0;
EXPECT_CALL(mock_subscribe_visitor_, OnPublishDone(kTrackName)).Times(0);
stream.Receive(framer_.SerializePublishDone(publish_done).AsStringView());
stream_visitor_->OnCanRead();
}
} // namespace
} // namespace moqt::test