/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ #include #include "gtest/gtest.h" #include "json/json.h" #include "mozilla/Preferences.h" #include "mozilla/Printf.h" #include "mozilla/XREAppData.h" #include "nsAppRunner.h" #include "nsDirectoryServiceDefs.h" #include "nsIFile.h" #include "nsIProperties.h" #include "nsIPropertyBag2.h" #include "nsString.h" #include "prtime.h" #include using mozilla::Preferences; using mozilla::PrefValueKind; class DowngradePingTest : public ::testing::Test { protected: const mozilla::XREAppData* mOriginalAppData = nullptr; mozilla::XREAppData mFakeAppData{}; mozilla::PathString mCreatedPingPath; nsCOMPtr mTmpAppDataDir; void SetUp() override { mFakeAppData.name = "TestApp"; mFakeAppData.version = "130.0"; mFakeAppData.buildID = "20250101000000"; mFakeAppData.vendor = "TestVendor"; mOriginalAppData = gAppData; gAppData = &mFakeAppData; nsCOMPtr tmpDir; ASSERT_EQ(NS_OK, NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpDir))); ASSERT_EQ(NS_OK, tmpDir->Append(u"downgrade-ping-test"_ns)); ASSERT_EQ(NS_OK, tmpDir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0755)); mTmpAppDataDir = tmpDir; nsCOMPtr dirSvc = do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID); ASSERT_TRUE(dirSvc); ASSERT_EQ(NS_OK, dirSvc->Set(XRE_USER_APP_DATA_DIR, mTmpAppDataDir)); ASSERT_EQ(NS_OK, Preferences::SetBool("datareporting.healthreport.uploadEnabled", true, PrefValueKind::User)); ASSERT_EQ(NS_OK, Preferences::SetCString("toolkit.telemetry.server", "https://incoming.telemetry.test"_ns, PrefValueKind::User)); ASSERT_EQ(NS_OK, Preferences::SetCString("toolkit.telemetry.cachedClientID", "test-client-id-1234"_ns, PrefValueKind::User)); ASSERT_EQ(NS_OK, Preferences::SetCString( "toolkit.telemetry.cachedProfileGroupID", "test-profile-group-id-5678"_ns, PrefValueKind::User)); } void TearDown() override { if (mOriginalAppData) { gAppData = mOriginalAppData; mOriginalAppData = nullptr; } Preferences::ClearUser("datareporting.healthreport.uploadEnabled"); Preferences::ClearUser("toolkit.telemetry.server"); Preferences::ClearUser("toolkit.telemetry.cachedClientID"); Preferences::ClearUser("toolkit.telemetry.cachedProfileGroupID"); RemoveUpdateTelemetryJson(); #ifdef XP_WIN RemoveInstallTelemetryJson(); #endif if (mTmpAppDataDir) { nsCOMPtr dirSvc = do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID); if (dirSvc) { dirSvc->Undefine(XRE_USER_APP_DATA_DIR); } mTmpAppDataDir->Remove(true); mTmpAppDataDir = nullptr; } } bool ReadPingFile(const mozilla::PathString& aPath, nsCString& aOutContents) { nsCOMPtr file; nsresult rv = NS_NewPathStringLocalFile(aPath, getter_AddRefs(file)); if (NS_FAILED(rv) || !file) return false; int64_t fileSize; rv = file->GetFileSize(&fileSize); if (NS_FAILED(rv)) return false; FILE* f = nullptr; rv = file->OpenANSIFileDesc("rb", &f); if (NS_FAILED(rv) || !f) return false; aOutContents.SetLength(fileSize); size_t bytesRead = fread(aOutContents.BeginWriting(), 1, fileSize, f); fclose(f); return bytesRead == static_cast(fileSize); } bool WriteUpdateTelemetryJson(uint64_t aTimestampMs) { nsCOMPtr greDir; if (NS_FAILED(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir)))) return false; nsCOMPtr file; if (NS_FAILED(greDir->Clone(getter_AddRefs(file)))) return false; if (NS_FAILED(file->Append(u"update_telemetry.json"_ns))) return false; FILE* f = nullptr; if (NS_FAILED(file->OpenANSIFileDesc("w", &f)) || !f) return false; fprintf(f, "{\"install_timestamp\":\"%" PRIu64 "\"}", aTimestampMs); fclose(f); return true; } void RemoveUpdateTelemetryJson() { nsCOMPtr greDir; if (NS_FAILED(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir)))) return; nsCOMPtr file; if (NS_FAILED(greDir->Clone(getter_AddRefs(file)))) return; if (NS_FAILED(file->Append(u"update_telemetry.json"_ns))) return; file->Remove(false); } #ifdef XP_WIN bool WriteInstallTelemetryJson(uint64_t aFileTime) { nsCOMPtr greDir; if (NS_FAILED(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir)))) return false; nsCOMPtr file; if (NS_FAILED(greDir->Clone(getter_AddRefs(file)))) return false; if (NS_FAILED(file->Append(u"installation_telemetry.json"_ns))) return false; char json[128]; SprintfLiteral(json, "{\"install_timestamp\":\"%" PRIu64 "\"}", aFileTime); nsAutoString utf16; utf16.AppendASCII(json); FILE* f = nullptr; if (NS_FAILED(file->OpenANSIFileDesc("wb", &f)) || !f) return false; fwrite(utf16.get(), sizeof(char16_t), utf16.Length(), f); fclose(f); return true; } void RemoveInstallTelemetryJson() { nsCOMPtr greDir; if (NS_FAILED(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir)))) return; nsCOMPtr file; if (NS_FAILED(greDir->Clone(getter_AddRefs(file)))) return; if (NS_FAILED(file->Append(u"installation_telemetry.json"_ns))) return; file->Remove(false); } #endif }; TEST_F(DowngradePingTest, MissingClientId) { Preferences::ClearUser("toolkit.telemetry.cachedClientID"); EXPECT_FALSE(GenerateDowngradeTelemetry( "test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0, "test-channel"_ns, "default"_ns, mozilla::Nothing(), false)); } TEST_F(DowngradePingTest, InvalidVersionFormat) { EXPECT_FALSE(GenerateDowngradeTelemetry( "test-ping-id"_ns, "131.0-no-underscore"_ns, false, 0, "test-channel"_ns, "default"_ns, mozilla::Nothing(), false)); } TEST_F(DowngradePingTest, FullPingStructure) { auto result = GenerateDowngradeTelemetry( "test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, true, 1, "test-channel"_ns, "default"_ns, mozilla::Nothing(), true); ASSERT_TRUE(result); mCreatedPingPath = *result; nsCString url; ASSERT_TRUE(BuildDowngradePingUrl("test-ping-id"_ns, "test-channel"_ns, url)); EXPECT_TRUE(StringBeginsWith(url, "https://incoming.telemetry.test"_ns)); EXPECT_NE(url.Find("/downgrade/"), kNotFound); nsCString contents; ASSERT_TRUE(ReadPingFile(*result, contents)); Json::Value root; Json::Reader reader; ASSERT_TRUE( reader.parse(contents.BeginReading(), contents.EndReading(), root)); EXPECT_STREQ(root["type"].asCString(), "downgrade"); EXPECT_STREQ(root["id"].asCString(), "test-ping-id"); { std::string dateStr = root["creationDate"].asString(); EXPECT_FALSE(dateStr.empty()); int year, month, day, hour, minute, second; EXPECT_EQ(6, sscanf(dateStr.c_str(), "%d-%d-%dT%d:%d:%d", &year, &month, &day, &hour, &minute, &second)); PRExplodedTime pingExploded = {}; pingExploded.tm_year = year; pingExploded.tm_month = month - 1; pingExploded.tm_mday = day; pingExploded.tm_hour = hour; pingExploded.tm_min = minute; pingExploded.tm_sec = second; pingExploded.tm_params = PR_GMTParameters(&pingExploded); PRTime pingTime = PR_ImplodeTime(&pingExploded); double diff = double(PR_Now() - pingTime) / PR_USEC_PER_SEC; EXPECT_GE(diff, 0); EXPECT_LT(diff, 600); } EXPECT_EQ(root["version"].asInt(), 4); EXPECT_STREQ(root["clientId"].asCString(), "test-client-id-1234"); EXPECT_STREQ(root["profileGroupId"].asCString(), "test-profile-group-id-5678"); Json::Value app = root["application"]; EXPECT_TRUE(app.isObject()); { nsCOMPtr sysInfo = do_GetService("@mozilla.org/system-info;1"); ASSERT_TRUE(sysInfo); nsAutoCString expectedArch; sysInfo->GetPropertyAsACString(u"arch"_ns, expectedArch); EXPECT_STREQ(app["architecture"].asCString(), expectedArch.get()); } EXPECT_STREQ(app["buildId"].asCString(), "20250101000000"); EXPECT_STREQ(app["name"].asCString(), "TestApp"); EXPECT_STREQ(app["version"].asCString(), "130.0"); EXPECT_STREQ(app["vendor"].asCString(), "TestVendor"); EXPECT_STREQ(app["channel"].asCString(), "test-channel"); Json::Value payload = root["payload"]; EXPECT_TRUE(payload.isObject()); EXPECT_STREQ(payload["lastVersion"].asCString(), "131.0"); EXPECT_STREQ(payload["lastBuildId"].asCString(), "20250201000000"); EXPECT_TRUE(payload["hasSync"].asBool()); EXPECT_EQ(payload["button"].asInt(), 1); EXPECT_TRUE(payload["isDifferentInstall"].asBool()); EXPECT_STREQ(payload["profileSelectionReason"].asCString(), "default"); } TEST_F(DowngradePingTest, PayloadBooleansFalse) { auto result = GenerateDowngradeTelemetry( "test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0, "test-channel"_ns, "restart"_ns, mozilla::Nothing(), false); ASSERT_TRUE(result); mCreatedPingPath = *result; nsCString contents; ASSERT_TRUE(ReadPingFile(*result, contents)); Json::Value root; Json::Reader reader; ASSERT_TRUE( reader.parse(contents.BeginReading(), contents.EndReading(), root)); Json::Value payload = root["payload"]; EXPECT_FALSE(payload["hasSync"].asBool()); EXPECT_EQ(payload["button"].asInt(), 0); EXPECT_FALSE(payload["isDifferentInstall"].asBool()); EXPECT_STREQ(payload["profileSelectionReason"].asCString(), "restart"); } TEST_F(DowngradePingTest, IsNewUpdateTrue) { // Update happened after the lock time. PRTime lockTime = PRTime(1700000000) * PR_MSEC_PER_SEC; uint64_t updateTimestampMs = 1701388800000ULL; if (!WriteUpdateTelemetryJson(updateTimestampMs)) return; auto result = GenerateDowngradeTelemetry( "test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0, "test-channel"_ns, "default"_ns, mozilla::Some(lockTime), false); ASSERT_TRUE(result); mCreatedPingPath = *result; nsCString contents; ASSERT_TRUE(ReadPingFile(*result, contents)); Json::Value root; Json::Reader reader; ASSERT_TRUE( reader.parse(contents.BeginReading(), contents.EndReading(), root)); Json::Value payload = root["payload"]; EXPECT_TRUE(payload.isMember("isNewUpdate")); EXPECT_TRUE(payload["isNewUpdate"].asBool()); } TEST_F(DowngradePingTest, IsNewUpdateFalse) { // Update happened before the lock time. uint64_t updateTimestampMs = 1700000000000ULL; PRTime lockTime = PRTime(1701388800) * PR_MSEC_PER_SEC; if (!WriteUpdateTelemetryJson(updateTimestampMs)) return; auto result = GenerateDowngradeTelemetry( "test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0, "test-channel"_ns, "default"_ns, mozilla::Some(lockTime), false); ASSERT_TRUE(result); mCreatedPingPath = *result; nsCString contents; ASSERT_TRUE(ReadPingFile(*result, contents)); Json::Value root; Json::Reader reader; ASSERT_TRUE( reader.parse(contents.BeginReading(), contents.EndReading(), root)); Json::Value payload = root["payload"]; EXPECT_TRUE(payload.isMember("isNewUpdate")); EXPECT_FALSE(payload["isNewUpdate"].asBool()); } TEST_F(DowngradePingTest, IsNewUpdateAbsentWithoutLockTime) { uint64_t updateTimestampMs = 1701388800000ULL; if (!WriteUpdateTelemetryJson(updateTimestampMs)) return; auto result = GenerateDowngradeTelemetry( "test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0, "test-channel"_ns, "default"_ns, mozilla::Nothing(), false); ASSERT_TRUE(result); mCreatedPingPath = *result; nsCString contents; ASSERT_TRUE(ReadPingFile(*result, contents)); Json::Value root; Json::Reader reader; ASSERT_TRUE( reader.parse(contents.BeginReading(), contents.EndReading(), root)); Json::Value payload = root["payload"]; EXPECT_FALSE(payload.isMember("isNewUpdate")); } TEST_F(DowngradePingTest, DaysSinceLock) { PRTime lockTime = PRTime(1700000000) * PR_MSEC_PER_SEC; auto result = GenerateDowngradeTelemetry( "test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0, "test-channel"_ns, "default"_ns, mozilla::Some(lockTime), false); ASSERT_TRUE(result); mCreatedPingPath = *result; nsCString contents; ASSERT_TRUE(ReadPingFile(*result, contents)); Json::Value root; Json::Reader reader; ASSERT_TRUE( reader.parse(contents.BeginReading(), contents.EndReading(), root)); Json::Value payload = root["payload"]; EXPECT_TRUE(payload.isMember("daysSinceLock")); int64_t expectedDays = (int64_t(time(nullptr)) - 1700000000) / 86400; int64_t actual = payload["daysSinceLock"].asInt64(); EXPECT_GE(actual, expectedDays - 1); EXPECT_LE(actual, expectedDays + 1); } #ifdef XP_WIN TEST_F(DowngradePingTest, IsNewInstallTrue) { // Install happened after the lock time. PRTime lockTime = PRTime(1700000000) * PR_MSEC_PER_SEC; constexpr uint64_t kEpochOffset = 116444736000000000ULL; int64_t installUnixSec = 1701388800; uint64_t installFileTime = uint64_t(installUnixSec) * PR_USEC_PER_SEC * 10 + kEpochOffset; if (!WriteInstallTelemetryJson(installFileTime)) return; auto result = GenerateDowngradeTelemetry( "test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0, "test-channel"_ns, "default"_ns, mozilla::Some(lockTime), false); ASSERT_TRUE(result); mCreatedPingPath = *result; nsCString contents; ASSERT_TRUE(ReadPingFile(*result, contents)); Json::Value root; Json::Reader reader; ASSERT_TRUE( reader.parse(contents.BeginReading(), contents.EndReading(), root)); Json::Value payload = root["payload"]; EXPECT_TRUE(payload.isMember("isNewInstall")); EXPECT_TRUE(payload["isNewInstall"].asBool()); } TEST_F(DowngradePingTest, IsNewInstallFalse) { // Install happened before the lock time. constexpr uint64_t kEpochOffset = 116444736000000000ULL; int64_t installUnixSec = 1700000000; uint64_t installFileTime = uint64_t(installUnixSec) * PR_USEC_PER_SEC * 10 + kEpochOffset; PRTime lockTime = PRTime(1701388800) * PR_MSEC_PER_SEC; if (!WriteInstallTelemetryJson(installFileTime)) return; auto result = GenerateDowngradeTelemetry( "test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0, "test-channel"_ns, "default"_ns, mozilla::Some(lockTime), false); ASSERT_TRUE(result); mCreatedPingPath = *result; nsCString contents; ASSERT_TRUE(ReadPingFile(*result, contents)); Json::Value root; Json::Reader reader; ASSERT_TRUE( reader.parse(contents.BeginReading(), contents.EndReading(), root)); Json::Value payload = root["payload"]; EXPECT_TRUE(payload.isMember("isNewInstall")); EXPECT_FALSE(payload["isNewInstall"].asBool()); } TEST_F(DowngradePingTest, IsNewInstallAbsentWithoutLockTime) { constexpr uint64_t kEpochOffset = 116444736000000000ULL; int64_t installUnixSec = 1701388800; uint64_t installFileTime = uint64_t(installUnixSec) * PR_USEC_PER_SEC * 10 + kEpochOffset; if (!WriteInstallTelemetryJson(installFileTime)) return; auto result = GenerateDowngradeTelemetry( "test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0, "test-channel"_ns, "default"_ns, mozilla::Nothing(), false); ASSERT_TRUE(result); mCreatedPingPath = *result; nsCString contents; ASSERT_TRUE(ReadPingFile(*result, contents)); Json::Value root; Json::Reader reader; ASSERT_TRUE( reader.parse(contents.BeginReading(), contents.EndReading(), root)); Json::Value payload = root["payload"]; EXPECT_FALSE(payload.isMember("isNewInstall")); } #else TEST_F(DowngradePingTest, IsNewInstallNotPresentOnNonWindows) { PRTime lockTime = PRTime(1700000000) * PR_MSEC_PER_SEC; auto result = GenerateDowngradeTelemetry( "test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0, "test-channel"_ns, "default"_ns, mozilla::Some(lockTime), false); ASSERT_TRUE(result); mCreatedPingPath = *result; nsCString contents; ASSERT_TRUE(ReadPingFile(*result, contents)); Json::Value root; Json::Reader reader; ASSERT_TRUE( reader.parse(contents.BeginReading(), contents.EndReading(), root)); Json::Value payload = root["payload"]; EXPECT_FALSE(payload.isMember("isNewInstall")); } #endif