Files
radiantwave/tests/test_license_service.cpp
NeilClack e7df02344b Kiosk: System Name storage, validation, sync, and sent on license validation
Adds local System Name persistence (trimmed, 1-40) in system_kv, an
injectable sync module with conflict/invalid revert and a pending-sync
flag, a background SystemNameSync worker (retries on boot and on a timer),
and api_client support for PUT /licenses/{key}/system-name plus the
additive systemName field/systemNameStatus on license validation. The
license service sends the stored name and applies the status. Covers
validation, pending retry, and conflict revert with a faked HTTP layer.
2026-09-29 09:57:52 -05:00

216 lines
6.9 KiB
C++

#include "network/license_service.h"
#include "db/db_keys.h"
#include "network/system_name.h"
#include <gtest/gtest.h>
#include <cstdlib>
#include <chrono>
#include <filesystem>
#include <string>
using namespace radiantwave::license;
using namespace radiantwave::db;
using namespace radiantwave::systemname;
namespace {
// A temporary database that is created fresh per test and cleaned up on exit.
struct TempDb {
TempDb() {
path = (std::filesystem::temp_directory_path() /
("test_rw_license_" + 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();
}
} // namespace
// ─── Offline: not-configured paths (no network) ────────────────────────
TEST(LicenseServiceTest, NotConfiguredWhenNoEmail) {
TempDb tmp;
tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP");
std::string errorMessage = "stale";
EXPECT_EQ(validateStoredLicense(tmp.db, &errorMessage),
Result::NotConfigured);
EXPECT_EQ(errorMessage, "");
}
TEST(LicenseServiceTest, NotConfiguredWhenNoLicense) {
TempDb tmp;
tmp.db.setKV(KV_EMAIL_ADDRESS, "user@example.com");
EXPECT_EQ(validateStoredLicense(tmp.db, nullptr),
Result::NotConfigured);
}
TEST(LicenseServiceTest, NotConfiguredWhenBothMissing) {
TempDb tmp;
EXPECT_EQ(validateStoredLicense(tmp.db), Result::NotConfigured);
}
// ─── Offline grace period ───────────────────────────────────────────────
long long epochSeconds(long long offsetSeconds = 0) {
const auto now = std::chrono::system_clock::now();
return std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch())
.count() +
offsetSeconds;
}
TEST(LicenseServiceTest, MarkVerifiedStampsNowAndGrantsGrace) {
TempDb tmp;
EXPECT_FALSE(tmp.db.hasKV(KV_LICENSE_LAST_VERIFIED));
EXPECT_FALSE(isLicenseWithinGracePeriod(tmp.db));
markLicenseVerified(tmp.db);
EXPECT_TRUE(tmp.db.hasKV(KV_LICENSE_LAST_VERIFIED));
EXPECT_TRUE(isLicenseWithinGracePeriod(tmp.db));
}
TEST(LicenseServiceTest, GracePeriodExpiresAfterSevenDays) {
TempDb tmp;
tmp.db.setKV(KV_LICENSE_LAST_VERIFIED, std::to_string(epochSeconds(-6 * 24 * 3600)));
EXPECT_TRUE(isLicenseWithinGracePeriod(tmp.db));
tmp.db.setKV(KV_LICENSE_LAST_VERIFIED, std::to_string(epochSeconds(-8 * 24 * 3600)));
EXPECT_FALSE(isLicenseWithinGracePeriod(tmp.db));
}
TEST(LicenseServiceTest, GracePeriodIgnoresCorruptStamp) {
TempDb tmp;
tmp.db.setKV(KV_LICENSE_LAST_VERIFIED, "not-a-timestamp");
EXPECT_FALSE(isLicenseWithinGracePeriod(tmp.db));
tmp.db.setKV(KV_LICENSE_LAST_VERIFIED, "-42");
EXPECT_FALSE(isLicenseWithinGracePeriod(tmp.db));
}
// ─── Offline: System Name status handling (faked validation) ────────────
namespace {
radiantwave::api::LicenseResult licenseResult(const std::string& systemNameStatus) {
radiantwave::api::LicenseResult result;
result.status = 200;
result.info.key = "ABCD-EFGH-IJKL-MNOP";
result.info.email_address = "user@example.com";
result.info.system_name_status = systemNameStatus;
return result;
}
} // namespace
TEST(LicenseServiceTest, SendsStoredSystemNameAndMarksSyncedOnOk) {
TempDb tmp;
tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP");
tmp.db.setKV(KV_EMAIL_ADDRESS, "user@example.com");
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
std::string sentName = "<none>";
const Result result = validateStoredLicenseWith(
tmp.db,
[&](const std::string&, const std::string&, const std::string& systemName) {
sentName = systemName;
return licenseResult("ok");
});
EXPECT_EQ(result, Result::Ok);
EXPECT_EQ(sentName, "Den");
EXPECT_FALSE(pendingSync(tmp.db));
EXPECT_EQ(tmp.db.getKV(KV_SYSTEM_NAME_SYNCED), "Den");
}
TEST(LicenseServiceTest, RevertsLocalNameOnConflictStatus) {
TempDb tmp;
tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP");
tmp.db.setKV(KV_EMAIL_ADDRESS, "user@example.com");
tmp.db.setKV(KV_SYSTEM_NAME_SYNCED, "Living Room");
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
const Result result = validateStoredLicenseWith(
tmp.db,
[](const std::string&, const std::string&, const std::string&) {
return licenseResult("conflict");
});
EXPECT_EQ(result, Result::Ok);
EXPECT_EQ(storedName(tmp.db), "Living Room");
EXPECT_EQ(tmp.db.getKV(KV_SYSTEM_NAME_MESSAGE), CONFLICT_MESSAGE);
}
TEST(LicenseServiceTest, LeavesPendingWhenServerReportsNoNameStatus) {
TempDb tmp;
tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP");
tmp.db.setKV(KV_EMAIL_ADDRESS, "user@example.com");
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
const Result result = validateStoredLicenseWith(
tmp.db,
[](const std::string&, const std::string&, const std::string&) {
return licenseResult("");
});
EXPECT_EQ(result, Result::Ok);
EXPECT_TRUE(pendingSync(tmp.db));
EXPECT_EQ(storedName(tmp.db), "Den");
}
// ─── Live: full validation + persistence (requires env credentials) ─────
TEST(LicenseServiceLiveTest, ValidatesAndGrantsSubscribedState) {
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);
std::string errorMessage;
const Result result = validateStoredLicense(tmp.db, &errorMessage);
EXPECT_EQ(result, Result::Ok) << "error=" << errorMessage;
// A successful validation is the "subscribed" flag; no subscription-level
// state is persisted. Only the "last verified" stamp that drives the
// offline grace window is written.
EXPECT_FALSE(tmp.db.hasKV("subscription_level_name"));
EXPECT_TRUE(tmp.db.hasKV(KV_LICENSE_LAST_VERIFIED));
EXPECT_TRUE(isLicenseWithinGracePeriod(tmp.db));
}
TEST(LicenseServiceLiveTest, InvalidPairReportsInvalid) {
TempDb tmp;
tmp.db.setKV(KV_LICENSE_KEY, "AAAA-AAAA-AAAA-AAAA");
tmp.db.setKV(KV_EMAIL_ADDRESS, "nobody@example.com");
std::string errorMessage;
EXPECT_EQ(validateStoredLicense(tmp.db, &errorMessage),
Result::Invalid);
EXPECT_FALSE(errorMessage.empty());
}