#include "runtime/payload_update_preflight.h" #include #include #include #include #include #include #include #include #include #include #include #ifndef MOCKTAIL_TEST_UPDATE_HELPER #error "MOCKTAIL_TEST_UPDATE_HELPER is required" #endif #ifndef MOCKTAIL_TEST_PROGRESS_HELPER #error "MOCKTAIL_TEST_PROGRESS_HELPER is required" #endif namespace mocktail::runtime { namespace { class MapEnvironment final : public Environment { public: explicit MapEnvironment(std::unordered_map values) : values_(std::move(values)) {} std::optional Get(std::string_view name) const override { const auto found = values_.find(std::string(name)); return found == values_.end() ? std::nullopt : std::optional(found->second); } private: std::unordered_map values_; }; class TemporaryDirectory final { public: TemporaryDirectory() { char pattern[] = "/tmp/mocktail_update_preflight_XXXXXX"; const char* created = mkdtemp(pattern); if (created != nullptr) root_ = created; } ~TemporaryDirectory() { std::error_code error; std::filesystem::remove_all(root_, error); } const std::filesystem::path& root() const { return root_; } private: std::filesystem::path root_; }; class ScopedVariable final { public: ScopedVariable(const char* name, const std::string& value) : name_(name) { const char* previous = std::getenv(name); if (previous != nullptr) previous_ = previous; EXPECT_EQ(setenv(name, value.c_str(), 1), 0); } ~ScopedVariable() { if (previous_.has_value()) { (void)setenv(name_.c_str(), previous_->c_str(), 1); } else { (void)unsetenv(name_.c_str()); } } private: std::string name_; std::optional previous_; }; std::vector ReadLines(const std::filesystem::path& path) { std::ifstream input(path); std::vector lines; for (std::string line; std::getline(input, line);) lines.push_back(line); return lines; } MapEnvironment NativeEnvironment( std::unordered_map additions = {}) { additions["MOCKTAIL_UPDATE_HELPER"] = MOCKTAIL_TEST_UPDATE_HELPER; return MapEnvironment(std::move(additions)); } TEST(PayloadUpdatePreflightTest, RunsNativeUpdater) { TemporaryDirectory temporary; const MapEnvironment environment = NativeEnvironment(); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.attempted); } TEST(PayloadUpdatePreflightTest, RunsForceLatestThroughNativeUpdater) { TemporaryDirectory temporary; const std::filesystem::path capture = temporary.root() / "argument"; ScopedVariable capture_variable("MOCKTAIL_TEST_UPDATE_ARGUMENT_CAPTURE", capture.string()); const MapEnvironment environment = NativeEnvironment(); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths, true); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.attempted); EXPECT_EQ(ReadLines(capture), std::vector({"--force-run-latest"})); } TEST(PayloadUpdatePreflightTest, RunsPackagedUpdaterFromProjectBin) { TemporaryDirectory temporary; const std::filesystem::path project_root = temporary.root() / "portable"; const std::filesystem::path helper = project_root / "bin/mocktail_updater"; ASSERT_TRUE(std::filesystem::create_directories(helper.parent_path())); ASSERT_TRUE(std::filesystem::copy_file(MOCKTAIL_TEST_UPDATE_HELPER, helper)); std::filesystem::permissions( helper, std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::add); const MapEnvironment environment( {{"MOCKTAIL_PROJECT_ROOT", project_root.string()}}); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.attempted); } TEST(PayloadUpdatePreflightTest, StreamsUpdaterStagesToNativeDialog) { TemporaryDirectory temporary; const std::filesystem::path capture = temporary.root() / "progress"; ScopedVariable capture_variable("MOCKTAIL_TEST_PROGRESS_CAPTURE", capture.string()); const MapEnvironment environment = NativeEnvironment({ {"MOCKTAIL_UPDATE_PROGRESS_HELPER", MOCKTAIL_TEST_PROGRESS_HELPER}, {"DISPLAY", ":test"}, }); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths); ASSERT_TRUE(result) << result.error; EXPECT_EQ(ReadLines(capture), std::vector({"PDownloading Roblox...", "S"})); } TEST(PayloadUpdatePreflightTest, PassesExactResolvedXdgPathsToUpdater) { TemporaryDirectory temporary; const std::filesystem::path home = temporary.root() / "home"; const std::filesystem::path capture = temporary.root() / "paths"; ScopedVariable capture_variable("MOCKTAIL_TEST_UPDATE_CAPTURE", capture.string()); const MapEnvironment environment = NativeEnvironment({{"HOME", home.string()}}); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths); ASSERT_TRUE(result) << result.error; EXPECT_EQ(ReadLines(capture), std::vector({ (home / ".config/mocktail/config.yaml").string(), (home / ".local/share/mocktail").string(), (home / ".cache/mocktail").string(), (home / ".local/state/mocktail").string(), })); } TEST(PayloadUpdatePreflightTest, ExplicitLibrarySkipsUpdater) { ScopedVariable exit_variable("MOCKTAIL_TEST_UPDATE_EXIT", "7"); const MapEnvironment environment = NativeEnvironment({{"ROBLOX_LIB_PATH", "/tmp/fixture.so"}}); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths); ASSERT_TRUE(result) << result.error; EXPECT_FALSE(result.attempted); } TEST(PayloadUpdatePreflightTest, RunnableManagedPayloadChecksForLatest) { TemporaryDirectory temporary; const std::filesystem::path data_root = temporary.root() / "data"; constexpr std::string_view kBuildId = "1686400865ae0e408cd7bd67de7a439625c6fd13"; const std::string payload_id = "2628-" + std::string(kBuildId); const std::filesystem::path payload_root = data_root / "payloads" / payload_id; ASSERT_TRUE( std::filesystem::create_directories(payload_root / "assets/content")); std::ofstream(payload_root / "libroblox.so") << "fixture"; std::ofstream(data_root / "current.json") << "{\"schema_version\":1,\"payload_id\":\"" << payload_id << "\",\"payload_path\":\"payloads/" << payload_id << "\",\"version_name\":\"2.727.1199\",\"version_code\":2628," "\"elf_build_id\":\"" << kBuildId << "\"}\n"; const MapEnvironment environment = NativeEnvironment({{"MOCKTAIL_DATA_ROOT", data_root.string()}}); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.attempted); } TEST(PayloadUpdatePreflightTest, LegacyProbationPayloadCrossesNativeUpdater) { TemporaryDirectory temporary; const std::filesystem::path data_root = temporary.root() / "data"; constexpr std::string_view kBuildId = "48fc8ee1fb36fc39072fd8619154ce90eea4b316"; constexpr std::string_view kGeneration = "0123456789abcdef0123456789abcdef01234567"; const std::string payload_id = "2718-" + std::string(kBuildId); const std::filesystem::path payload_root = data_root / "payloads" / payload_id; ASSERT_TRUE( std::filesystem::create_directories(payload_root / "assets/content")); std::ofstream(payload_root / "libroblox.so") << "fixture"; const std::string artifact = payload_id + "-" + std::string(kGeneration) + ".json"; for (const std::string_view directory : {"approvals", "host_abi_profiles", "compatibility_profiles"}) { ASSERT_TRUE(std::filesystem::create_directories(data_root / directory)); std::ofstream(data_root / directory / artifact) << "{}\n"; } std::ofstream(data_root / "current.json") << "{\"schema_version\":1,\"payload_id\":\"" << payload_id << "\",\"payload_path\":\"payloads/" << payload_id << "\",\"version_name\":\"2.730.790\",\"version_code\":2718," "\"elf_build_id\":\"" << kBuildId << "\",\"approval_path\":\"approvals/" << artifact << "\",\"host_abi_profile_path\":\"host_abi_profiles/" << artifact << "\",\"compatibility_manifest_path\":\"compatibility_profiles/" << artifact << "\"}\n"; const MapEnvironment environment = NativeEnvironment({{"MOCKTAIL_DATA_ROOT", data_root.string()}}); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.attempted); } TEST(PayloadUpdatePreflightTest, RejectsRelativeUpdaterOverride) { const MapEnvironment environment( {{"MOCKTAIL_UPDATE_HELPER", "build/mocktail_updater"}}); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths); ASSERT_TRUE(result) << result.error; EXPECT_FALSE(result.attempted); } TEST(PayloadUpdatePreflightTest, PropagatesUpdaterFailure) { ScopedVariable exit_variable("MOCKTAIL_TEST_UPDATE_EXIT", "7"); const MapEnvironment environment = NativeEnvironment(); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths); EXPECT_FALSE(result); EXPECT_TRUE(result.attempted); EXPECT_EQ(result.error, "Roblox update preflight failed"); } TEST(PayloadUpdatePreflightTest, ReportsUpdaterFailureDetails) { ScopedVariable exit_variable("MOCKTAIL_TEST_UPDATE_EXIT", "1"); ScopedVariable stderr_variable( "MOCKTAIL_TEST_UPDATE_STDERR", "[native-updater] latest Roblox failed probation (api.pureapk.com " "request returned status 503)"); const MapEnvironment environment = NativeEnvironment(); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths); EXPECT_FALSE(result); EXPECT_EQ(result.error, "Roblox update preflight failed"); EXPECT_EQ(result.details, "latest Roblox failed probation (api.pureapk.com request returned " "status 503)"); } TEST(PayloadUpdatePreflightTest, KeepsLastUpdaterFailureOverWarnings) { ScopedVariable exit_variable("MOCKTAIL_TEST_UPDATE_EXIT", "1"); ScopedVariable stderr_variable( "MOCKTAIL_TEST_UPDATE_STDERR", "[native-updater] warning: configuration key is unknown\n" "unrelated diagnostic line\n" "[native-updater] supported fallback also failed"); const MapEnvironment environment = NativeEnvironment(); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths); EXPECT_FALSE(result); EXPECT_EQ(result.details, "supported fallback also failed"); } TEST(PayloadUpdatePreflightTest, LeavesDetailsEmptyWhenUpdaterSucceeds) { ScopedVariable stderr_variable( "MOCKTAIL_TEST_UPDATE_STDERR", "[native-updater] installed exact-supported Roblox 2.727.1199 (2628)"); const MapEnvironment environment = NativeEnvironment(); const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); const auto result = RunPayloadUpdatePreflight(environment, paths); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.details.empty()); } } // namespace } // namespace mocktail::runtime