#include "network/license_status.h" #include "db/db_keys.h" #include #include #include #include #include #include using namespace radiantwave::license; using namespace radiantwave::db; namespace { // A temporary database created fresh per test and cleaned up on exit. struct TempDb { TempDb() { path = (std::filesystem::temp_directory_path() / ("test_rw_license_status_" + std::to_string(counter++) + ".db")) .string(); std::filesystem::remove(path); db.initialize(path); } ~TempDb() { db.close(); std::filesystem::remove(path); } std::string path; DatabaseManager db; static int counter; }; int TempDb::counter = 0; std::string env(const char* name) { const char* value = std::getenv(name); return value ? std::string(value) : std::string(); } // Waits until `status()` no longer reports Checking (or `timeout` elapses). // Returns the final status. Status waitForResult(LicenseStatusTracker& tracker, int timeoutMs = 2000) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); while (tracker.status() == Status::Checking) { if (std::chrono::steady_clock::now() >= deadline) break; std::this_thread::sleep_for(std::chrono::milliseconds(10)); } return tracker.status(); } } // namespace // ─── Pure mapping (offline) ───────────────────────────────────────────── TEST(LicenseStatusTest, StatusForResultMapping) { EXPECT_EQ(statusForResult(Result::Ok), Status::Valid); EXPECT_EQ(statusForResult(Result::NotConfigured), Status::Invalid); EXPECT_EQ(statusForResult(Result::Invalid), Status::Invalid); EXPECT_EQ(statusForResult(Result::NetworkError), Status::Invalid); } long long epochSeconds(long long offsetSeconds = 0) { const auto now = std::chrono::system_clock::now(); return std::chrono::duration_cast(now.time_since_epoch()) .count() + offsetSeconds; } TEST(LicenseStatusTest, StatusForResultWithGraceNoPriorValidation) { TempDb tmp; // No "last verified" stamp: a network error still invalidates because the // device has no record of ever confirming the license. EXPECT_EQ(statusForResultWithGrace(Result::Ok, tmp.db), Status::Valid); EXPECT_EQ(statusForResultWithGrace(Result::NotConfigured, tmp.db), Status::Invalid); EXPECT_EQ(statusForResultWithGrace(Result::Invalid, tmp.db), Status::Invalid); EXPECT_EQ(statusForResultWithGrace(Result::NetworkError, tmp.db), Status::Invalid); } TEST(LicenseStatusTest, StatusForResultWithGraceWithinWindow) { TempDb tmp; tmp.db.setKV(KV_LICENSE_LAST_VERIFIED, std::to_string(epochSeconds())); // A network error keeps a recently-confirmed license valid... EXPECT_EQ(statusForResultWithGrace(Result::NetworkError, tmp.db), Status::Valid); // ...but the grace period never rescues an explicit rejection. EXPECT_EQ(statusForResultWithGrace(Result::Invalid, tmp.db), Status::Invalid); EXPECT_EQ(statusForResultWithGrace(Result::NotConfigured, tmp.db), Status::Invalid); } TEST(LicenseStatusTest, StatusForResultWithGraceExpired) { TempDb tmp; tmp.db.setKV(KV_LICENSE_LAST_VERIFIED, std::to_string(epochSeconds(-8 * 24 * 3600))); EXPECT_EQ(statusForResultWithGrace(Result::NetworkError, tmp.db), Status::Invalid); } // ─── Scheduling policy (pure) ─────────────────────────────────────────── TEST(LicenseStatusPolicyTest, ForcedAlwaysRuns) { EXPECT_TRUE(shouldValidate(true, false, false, Status::Invalid, 1000, 999)); EXPECT_TRUE(shouldValidate(true, false, false, Status::Unknown, 1000, 0)); } TEST(LicenseStatusPolicyTest, CredentialChangeRuns) { EXPECT_TRUE(shouldValidate(false, true, false, Status::Invalid, 1000, 999)); } TEST(LicenseStatusPolicyTest, OverdueRecheckRuns) { EXPECT_TRUE(shouldValidate(false, false, true, Status::Invalid, 1000, 999)); } TEST(LicenseStatusPolicyTest, InvalidUnchangedDoesNotRun) { // An invalid license with unchanged credentials is never re-checked, no // matter how old the last attempt is. EXPECT_FALSE(shouldValidate(false, false, false, Status::Invalid, 1000, 999)); EXPECT_FALSE(shouldValidate(false, false, false, Status::Invalid, 1000, 1000 - 10 * RECHECK_INTERVAL_SECONDS)); EXPECT_FALSE(shouldValidate(false, false, false, Status::Unknown, 1000, 0)); } TEST(LicenseStatusPolicyTest, ValidRechecksAfterInterval) { const long long now = 1000000; EXPECT_FALSE(shouldValidate(false, false, false, Status::Valid, now, now - 10)); EXPECT_FALSE(shouldValidate(false, false, false, Status::Valid, now, now - (RECHECK_INTERVAL_SECONDS - 1))); EXPECT_TRUE(shouldValidate(false, false, false, Status::Valid, now, now - RECHECK_INTERVAL_SECONDS)); // Off for several hours: still immediately due, not another full hour away. EXPECT_TRUE(shouldValidate(false, false, false, Status::Valid, now, now - 5 * RECHECK_INTERVAL_SECONDS)); } TEST(LicenseStatusPolicyTest, ValidWithMissingStampRechecks) { EXPECT_TRUE(shouldValidate(false, false, false, Status::Valid, 1000, 0)); } // ─── Worker lifecycle (no network) ────────────────────────────────────── TEST(LicenseStatusTest, UnknownBeforeStart) { LicenseStatusTracker tracker; EXPECT_EQ(tracker.status(), Status::Unknown); EXPECT_FALSE(tracker.isValid()); // refresh() before start() must not spawn a worker. tracker.refresh(); EXPECT_EQ(tracker.status(), Status::Unknown); } TEST(LicenseStatusTest, InvalidWhenNoCredentials) { TempDb tmp; LicenseStatusTracker tracker; tracker.start(tmp.db); tracker.refresh(); // NotConfigured resolves without touching the network. EXPECT_EQ(waitForResult(tracker), Status::Invalid); EXPECT_FALSE(tracker.isValid()); tracker.shutdown(); } TEST(LicenseStatusTest, InvalidWhenDatabaseClosed) { TempDb tmp; LicenseStatusTracker tracker; tracker.start(tmp.db); tracker.refresh(); EXPECT_EQ(waitForResult(tracker), Status::Invalid); tracker.shutdown(); } TEST(LicenseStatusTest, LoadsPersistedValidWithinGraceWithoutChecking) { TempDb tmp; tmp.db.setKV(KV_LICENSE_KEY, "k"); tmp.db.setKV(KV_EMAIL_ADDRESS, "e"); tmp.db.setKV(KV_LICENSE_CHECKED_KEY, "k"); tmp.db.setKV(KV_LICENSE_CHECKED_EMAIL, "e"); tmp.db.setKV(KV_LICENSE_CHECK_STATUS, "valid"); tmp.db.setKV(KV_LICENSE_LAST_VERIFIED, std::to_string(epochSeconds())); tmp.db.setKV(KV_LICENSE_LAST_CHECK, std::to_string(epochSeconds())); LicenseStatusTracker tracker; tracker.start(tmp.db); // Seeded valid + recently checked + unchanged credentials: the worker must // stay idle (no network), and the status is valid immediately. EXPECT_EQ(tracker.status(), Status::Valid); tracker.shutdown(); } TEST(LicenseStatusTest, RecordResultUpdatesStatusAndPersistsCheckedPair) { TempDb tmp; const std::string key = "AAAA-BBBB-CCCC-DDDD"; const std::string email = "user@example.com"; tmp.db.setKV(KV_LICENSE_KEY, key); tmp.db.setKV(KV_EMAIL_ADDRESS, email); // Seed the checked pair as already-invalid so the worker does not start a // network check; this exercises recordResult() in isolation. tmp.db.setKV(KV_LICENSE_CHECKED_KEY, key); tmp.db.setKV(KV_LICENSE_CHECKED_EMAIL, email); tmp.db.setKV(KV_LICENSE_CHECK_STATUS, "invalid"); LicenseStatusTracker tracker; tracker.start(tmp.db); EXPECT_EQ(tracker.status(), Status::Invalid); tracker.recordResult(Result::Ok); EXPECT_EQ(tracker.status(), Status::Valid); EXPECT_TRUE(tracker.isValid()); EXPECT_EQ(tmp.db.getKV(KV_LICENSE_CHECKED_KEY), key); EXPECT_EQ(tmp.db.getKV(KV_LICENSE_CHECKED_EMAIL), email); EXPECT_EQ(tmp.db.getKV(KV_LICENSE_CHECK_STATUS), "valid"); EXPECT_FALSE(tmp.db.getKV(KV_LICENSE_LAST_CHECK).empty()); tracker.shutdown(); } // ─── Live: confirmed-valid license (requires env credentials) ─────────── TEST(LicenseStatusLiveTest, ValidWhenLicensePairAccepted) { const std::string key = env("RW_LIVE_LICENSE_KEY"); const std::string email = env("RW_LIVE_LICENSE_EMAIL"); if (key.empty() || email.empty()) { GTEST_SKIP() << "RW_LIVE_LICENSE_KEY / RW_LIVE_LICENSE_EMAIL not set"; } TempDb tmp; tmp.db.setKV(KV_LICENSE_KEY, key); tmp.db.setKV(KV_EMAIL_ADDRESS, email); LicenseStatusTracker tracker; tracker.start(tmp.db); tracker.refresh(); EXPECT_EQ(waitForResult(tracker, 20000), Status::Valid); EXPECT_TRUE(tracker.isValid()); tracker.shutdown(); }