blob: 9da725382da7767f605bf892932294f4473f62f6 [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_bidi_stream.h"
#include <memory>
#include <optional>
#include <utility>
#include <variant>
#include "absl/status/status.h"
#include "absl/strings/string_view.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_parser.h"
#include "quiche/quic/moqt/moqt_session_interface.h"
#include "quiche/quic/moqt/test_tools/mock_moqt_session.h"
#include "quiche/quic/moqt/test_tools/moqt_framer_utils.h"
#include "quiche/common/platform/api/quiche_test.h"
#include "quiche/common/test_tools/quiche_test_utils.h"
#include "quiche/web_transport/test_tools/in_memory_stream.h"
#include "quiche/web_transport/test_tools/mock_web_transport.h"
namespace moqt::test {
class TestMoqtBidiStream : public MoqtBidiStreamBase {
public:
using MoqtBidiStreamBase::MoqtBidiStreamBase;
void OnStreamBound() override {}
absl::Status OnRawControlMessage(
const MoqtRawControlMessage& message) override {
return ControlMessageDispatcher::DispatchControlMessage(
*this, message_parser(), message, "test");
}
void Detach() override { detached_ = true; }
bool detached_ = false;
absl::Status OnControlMessage(const MoqtRequestOk& message) {
return MoqtBidiStreamBase::OnControlMessage(message);
}
absl::Status OnControlMessage(const MoqtRequestError& message) {
return MoqtBidiStreamBase::OnControlMessage(message);
}
};
class MoqtBidiStreamTest : public quiche::test::QuicheTest {
public:
MoqtBidiStreamTest()
: framer_(true, quic::Perspective::IS_CLIENT),
stream_(std::make_unique<TestMoqtBidiStream>(
&framer_,
MoqtControlMessageParser(kDefaultMoqtVersion,
/*webtransport=*/true,
quic::Perspective::IS_CLIENT),
error_callback_.AsStdFunction())) {}
MoqtFramer framer_;
testing::StrictMock<testing::MockFunction<void(MoqtError, absl::string_view)>>
error_callback_;
std::unique_ptr<TestMoqtBidiStream> stream_;
webtransport::test::MockStream mock_stream_;
};
TEST_F(MoqtBidiStreamTest, Reset) {
stream_->BindStream(&mock_stream_);
EXPECT_CALL(mock_stream_, ResetWithUserCode(1234));
stream_->Reset(1234);
EXPECT_TRUE(stream_->detached_);
}
TEST_F(MoqtBidiStreamTest, IncomingReset) {
stream_->BindStream(&mock_stream_);
EXPECT_CALL(mock_stream_, ResetWithUserCode(1234));
stream_->OnResetStreamReceived(1234);
EXPECT_TRUE(stream_->detached_);
}
TEST_F(MoqtBidiStreamTest, FinDetaches) {
stream_->BindStream(&mock_stream_);
stream_->Fin();
EXPECT_TRUE(stream_->detached_);
}
TEST_F(MoqtBidiStreamTest, IncomingStopSending) {
stream_->BindStream(&mock_stream_);
EXPECT_CALL(mock_stream_, ResetWithUserCode(1234));
stream_->OnStopSendingReceived(1234);
EXPECT_TRUE(stream_->detached_);
}
TEST_F(MoqtBidiStreamTest, SendRequestError) {
stream_->BindStream(&mock_stream_);
EXPECT_CALL(mock_stream_, CanWrite).WillRepeatedly(testing::Return(true));
EXPECT_CALL(
mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestError), testing::_));
QUICHE_EXPECT_OK(stream_->SendRequestError(
1,
MoqtRequestErrorInfo{RequestErrorCode::kUnauthorized,
/*retry_interval=*/std::nullopt, ""},
false));
EXPECT_FALSE(stream_->detached_);
EXPECT_CALL(
mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestError), testing::_));
QUICHE_EXPECT_OK(stream_->SendRequestError(
1,
MoqtRequestErrorInfo{RequestErrorCode::kUnauthorized,
/*retry_interval=*/std::nullopt, ""},
true));
EXPECT_TRUE(stream_->detached_);
}
TEST_F(MoqtBidiStreamTest, DispatchControlMessage) {
webtransport::test::InMemoryStream stream(0);
stream_->BindStream(&stream);
MoqtFramer framer(/*using_webtrans=*/true, quic::Perspective::IS_SERVER);
stream.Receive(framer.SerializeRequestOk(MoqtRequestOk()).AsStringView());
stream_->OnCanRead();
stream.Receive(framer.SerializeGoAway(MoqtGoAway()).AsStringView());
EXPECT_CALL(error_callback_, Call)
.WillOnce([](MoqtError error, absl::string_view message) {
EXPECT_EQ(error, MoqtError::kProtocolViolation);
EXPECT_EQ(
message,
"Received an unexpected message of type GOAWAY on a test stream");
});
stream_->OnCanRead();
}
TEST_F(MoqtBidiStreamTest, SendRequestOk) {
stream_->BindStream(&mock_stream_);
EXPECT_CALL(mock_stream_, CanWrite).WillRepeatedly(testing::Return(true));
EXPECT_CALL(
mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestOk), testing::_));
MessageParameters parameters;
parameters.subscriber_priority = 20;
QUICHE_EXPECT_OK(stream_->SendRequestOk(1, parameters, /*fin=*/false));
EXPECT_FALSE(stream_->detached_);
}
TEST_F(MoqtBidiStreamTest, SendRequestOkFin) {
stream_->BindStream(&mock_stream_);
EXPECT_CALL(mock_stream_, CanWrite).WillRepeatedly(testing::Return(true));
EXPECT_CALL(
mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestOk), testing::_));
MessageParameters parameters;
QUICHE_EXPECT_OK(stream_->SendRequestOk(1, parameters, /*fin=*/true));
EXPECT_TRUE(stream_->detached_);
}
TEST_F(MoqtBidiStreamTest, SendRequestErrorOverload) {
stream_->BindStream(&mock_stream_);
EXPECT_CALL(mock_stream_, CanWrite).WillRepeatedly(testing::Return(true));
EXPECT_CALL(
mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestError), testing::_));
QUICHE_EXPECT_OK(stream_->SendRequestError(1, RequestErrorCode::kUnauthorized,
std::nullopt, "reason",
/*fin=*/true));
EXPECT_TRUE(stream_->detached_);
}
TEST_F(MoqtBidiStreamTest, SendRequestUpdateAndReceiveOk) {
stream_->BindStream(&mock_stream_);
EXPECT_CALL(mock_stream_, CanWrite).WillRepeatedly(testing::Return(true));
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestUpdate),
testing::_));
MessageParameters parameters;
parameters.subscriber_priority = 20;
bool callback_called = false;
MoqtResponseCallback callback =
[&](std::variant<MessageParameters, MoqtRequestErrorInfo> res) {
callback_called = true;
ASSERT_TRUE(std::holds_alternative<MessageParameters>(res));
EXPECT_EQ(std::get<MessageParameters>(res).subscriber_priority, 30);
};
QUICHE_EXPECT_OK(
stream_->SendRequestUpdate(1, 0, parameters, std::move(callback)));
// Simulate receiving RequestOk
MoqtRequestOk request_ok;
request_ok.request_id = 1;
request_ok.parameters.subscriber_priority = 30;
QUICHE_EXPECT_OK(stream_->OnControlMessage(request_ok));
EXPECT_TRUE(callback_called);
EXPECT_FALSE(stream_->detached_);
}
TEST_F(MoqtBidiStreamTest, SendRequestUpdateAndReceiveError) {
stream_->BindStream(&mock_stream_);
EXPECT_CALL(mock_stream_, CanWrite).WillRepeatedly(testing::Return(true));
EXPECT_CALL(mock_stream_,
Writev(ControlMessageOfType(MoqtMessageType::kRequestUpdate),
testing::_));
MessageParameters parameters;
bool callback_called = false;
MoqtResponseCallback callback =
[&](std::variant<MessageParameters, MoqtRequestErrorInfo> res) {
callback_called = true;
ASSERT_TRUE(std::holds_alternative<MoqtRequestErrorInfo>(res));
EXPECT_EQ(std::get<MoqtRequestErrorInfo>(res).error_code,
RequestErrorCode::kUnauthorized);
};
QUICHE_EXPECT_OK(
stream_->SendRequestUpdate(1, 0, parameters, std::move(callback)));
// Simulate receiving RequestError
MoqtRequestError request_error;
request_error.request_id = 1;
request_error.error_code = RequestErrorCode::kUnauthorized;
request_error.reason_phrase = "unauthorized";
ExpectFin(mock_stream_);
QUICHE_EXPECT_OK(stream_->OnControlMessage(request_error));
EXPECT_TRUE(callback_called);
EXPECT_TRUE(stream_->detached_);
}
TEST_F(MoqtBidiStreamTest, QueueIsFull) {
stream_->BindStream(&mock_stream_);
EXPECT_FALSE(stream_->QueueIsFull());
}
} // namespace moqt::test