blob: 155e97a7e2f1d2bcca380b920f7781c533a61267 [file]
// Copyright 2026 The Fuchsia 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 "src/developer/forensics/feedback/attachments/previous_boot_inspect.h"
#include <fcntl.h>
#include <lib/async/cpp/executor.h>
#include <lib/fdio/fd.h>
#include <lib/fpromise/result.h>
#include <lib/inspect/cpp/vmo/types.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/developer/forensics/feedback/attachments/types.h"
#include "src/developer/forensics/testing/backoff.h"
#include "src/developer/forensics/testing/gmatchers.h"
#include "src/developer/forensics/testing/stubs/previous_boot_data_provider.h"
#include "src/developer/forensics/testing/unit_test_fixture.h"
#include "src/developer/forensics/utils/errors.h"
#include "src/developer/forensics/utils/redact/redactor.h"
#include "src/lib/files/file.h"
#include "src/lib/files/scoped_temp_dir.h"
namespace forensics::feedback {
namespace {
class PreviousBootDataProviderClosesConnectionThenReturnsData
: public stubs::PreviousBootDataProviderBase {
public:
explicit PreviousBootDataProviderClosesConnectionThenReturnsData(
fuchsia::diagnostics::persistence::PreviousBootData data)
: data_(std::move(data)) {}
void WatchPreviousBootData(
fuchsia::diagnostics::persistence::PreviousBootDataProviderOptions options,
WatchPreviousBootDataCallback callback) override {
if (!has_closed_) {
has_closed_ = true;
CloseConnection();
return;
}
fuchsia::diagnostics::persistence::PreviousBootDataProvider_WatchPreviousBootData_Response
response;
response.data = std::move(data_);
callback(
fuchsia::diagnostics::persistence::PreviousBootDataProvider_WatchPreviousBootData_Result::
WithResponse(std::move(response)));
}
private:
bool has_closed_ = false;
fuchsia::diagnostics::persistence::PreviousBootData data_;
};
class PreviousBootInspectTest : public UnitTestFixture {
public:
PreviousBootInspectTest()
: executor_(dispatcher()),
redactor_(std::make_unique<IdentityRedactor>(inspect::BoolProperty())) {}
protected:
void SetUpDataProviderServer(std::unique_ptr<stubs::PreviousBootDataProviderBase> server) {
server_ = std::move(server);
if (server_) {
InjectServiceProvider(server_.get());
}
}
fidl::InterfaceHandle<fuchsia::io::File> CreateFileHandle(const std::string& content) {
std::string path;
dir_.NewTempFileWithData(content, &path);
int fd = open(path.c_str(), O_RDONLY);
FX_CHECK(fd >= 0);
zx_handle_t handle = ZX_HANDLE_INVALID;
FX_CHECK(fdio_fd_transfer(fd, &handle) == ZX_OK);
return fidl::InterfaceHandle<fuchsia::io::File>(zx::channel(handle));
}
AttachmentData Run(::fpromise::promise<AttachmentData> promise) {
AttachmentData attachment(Error::kNotSet);
executor_.schedule_task(
promise.and_then([&attachment](AttachmentData& result) { attachment = std::move(result); })
.or_else([]() { FX_LOGS(FATAL) << "Unexpected branch"; }));
RunLoopUntilIdle();
return attachment;
}
RedactorBase* GetRedactor() const { return redactor_.get(); }
void SetRedactor(std::unique_ptr<RedactorBase> redactor) { redactor_ = std::move(redactor); }
async::Executor& GetExecutor() { return executor_; }
private:
async::Executor executor_;
std::unique_ptr<stubs::PreviousBootDataProviderBase> server_;
std::unique_ptr<RedactorBase> redactor_;
files::ScopedTempDir dir_;
};
TEST_F(PreviousBootInspectTest, SucceedsReturnsInspectData) {
const std::string kInspectData = "[{\"root\": {\"value\": 123}}]";
fuchsia::diagnostics::persistence::PreviousBootData data;
data.set_inspect(CreateFileHandle(kInspectData));
SetUpDataProviderServer(
std::make_unique<stubs::PreviousBootDataProviderReturnsData>(std::move(data)));
PreviousBootInspect provider(dispatcher(), services(), std::make_unique<MonotonicBackoff>(),
GetRedactor());
AttachmentData result = Run(provider.Get(1));
EXPECT_THAT(result, AttachmentDataIs(kInspectData));
}
TEST_F(PreviousBootInspectTest, FailsMissingInspect) {
fuchsia::diagnostics::persistence::PreviousBootData data;
SetUpDataProviderServer(
std::make_unique<stubs::PreviousBootDataProviderReturnsData>(std::move(data)));
PreviousBootInspect provider(dispatcher(), services(), std::make_unique<MonotonicBackoff>(),
GetRedactor());
AttachmentData result = Run(provider.Get(1));
EXPECT_THAT(result, AttachmentDataIs(Error::kMissingValue));
}
TEST_F(PreviousBootInspectTest, ReconnectsOnConnectionClosed) {
auto server = std::make_unique<stubs::PreviousBootDataProviderNeverReturns>();
auto* server_ptr = server.get();
SetUpDataProviderServer(std::move(server));
PreviousBootInspect provider(dispatcher(), services(), std::make_unique<MonotonicBackoff>(),
GetRedactor());
RunLoopUntilIdle();
EXPECT_TRUE(server_ptr->IsBound());
server_ptr->CloseConnection(ZX_ERR_PEER_CLOSED);
RunLoopUntilIdle();
EXPECT_FALSE(server_ptr->IsBound());
RunLoopFor(zx::sec(1));
EXPECT_TRUE(server_ptr->IsBound());
}
TEST_F(PreviousBootInspectTest, ReconnectsReturnsDataAfterRetry) {
const std::string kInspectData = "[{\"root\": {\"value\": 123}}]";
fuchsia::diagnostics::persistence::PreviousBootData data;
data.set_inspect(CreateFileHandle(kInspectData));
SetUpDataProviderServer(
std::make_unique<PreviousBootDataProviderClosesConnectionThenReturnsData>(std::move(data)));
PreviousBootInspect provider(dispatcher(), services(), std::make_unique<MonotonicBackoff>(),
GetRedactor());
AttachmentData result(Error::kNotSet);
GetExecutor().schedule_task(
provider.Get(1)
.and_then([&result](AttachmentData& val) { result = std::move(val); })
.or_else([]() { FX_LOGS(FATAL) << "Unexpected branch"; }));
RunLoopUntilIdle();
EXPECT_FALSE(result.HasValue());
RunLoopFor(zx::sec(1));
EXPECT_THAT(result, AttachmentDataIs(kInspectData));
}
TEST_F(PreviousBootInspectTest, CachedResultMultipleGets) {
const std::string kInspectData = "[{\"root\": {\"value\": 456}}]";
fuchsia::diagnostics::persistence::PreviousBootData data;
data.set_inspect(CreateFileHandle(kInspectData));
SetUpDataProviderServer(
std::make_unique<stubs::PreviousBootDataProviderReturnsData>(std::move(data)));
PreviousBootInspect provider(dispatcher(), services(), std::make_unique<MonotonicBackoff>(),
GetRedactor());
AttachmentData result1 = Run(provider.Get(1));
EXPECT_THAT(result1, AttachmentDataIs(kInspectData));
// Second call should return cached value immediately without needing the server.
AttachmentData result2 = Run(provider.Get(2));
EXPECT_THAT(result2, AttachmentDataIs(kInspectData));
}
TEST_F(PreviousBootInspectTest, ForceCompletionTimeout) {
SetUpDataProviderServer(std::make_unique<stubs::PreviousBootDataProviderNeverReturns>());
PreviousBootInspect provider(dispatcher(), services(), std::make_unique<MonotonicBackoff>(),
GetRedactor());
AttachmentData result(Error::kNotSet);
GetExecutor().schedule_task(
provider.Get(1)
.and_then([&result](AttachmentData& val) { result = std::move(val); })
.or_else([]() { FX_LOGS(FATAL) << "Unexpected branch"; }));
RunLoopUntilIdle();
EXPECT_FALSE(result.HasValue());
EXPECT_EQ(result.Error(), Error::kNotSet);
provider.ForceCompletion(1, Error::kTimeout);
RunLoopUntilIdle();
EXPECT_THAT(result, AttachmentDataIs(Error::kTimeout));
}
TEST_F(PreviousBootInspectTest, RedactsWithJsonReplacers) {
const std::string kInspectData =
"[\"1.2.3.4\",\n" // IPv4 Addresses are redacted
"\"5.6.7.8\",\n"
"\"2001::1\",\n" // IPv6 Addresses are redacted
"\"2001::2\",\n"
"\"AA-BB-CC-DD-EE-FF\",\n" // MAC Addresses are redacted with manufacturer component
// unredacted
"\"11:22:33:44:55:66\",\n"
"1234567890abcdefABCDEF0123456789,\n" // Long Hex numbers are not redacted
"\"106986199446298680449\"]"; // Obfuscated Gaia IDs are not redacted
fuchsia::diagnostics::persistence::PreviousBootData data;
data.set_inspect(CreateFileHandle(kInspectData));
SetUpDataProviderServer(
std::make_unique<stubs::PreviousBootDataProviderReturnsData>(std::move(data)));
inspect::BoolProperty redaction_enabled;
redaction_enabled.Set(true);
SetRedactor(std::make_unique<Redactor>(0, inspect::UintProperty(), std::move(redaction_enabled)));
PreviousBootInspect provider(dispatcher(), services(), std::make_unique<MonotonicBackoff>(),
GetRedactor());
AttachmentData result = Run(provider.Get(1));
EXPECT_THAT(result, AttachmentDataIs(R"(["<REDACTED-IPV4: 1>",
"<REDACTED-IPV4: 2>",
"<REDACTED-IPV6: 3>",
"<REDACTED-IPV6: 4>",
"AA-BB-CC-<REDACTED-MAC: 5>",
"11:22:33:<REDACTED-MAC: 6>",
1234567890abcdefABCDEF0123456789,
"106986199446298680449"])"));
}
} // namespace
} // namespace forensics::feedback