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.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#include "network/license_service.h"
|
||||
#include "db/db_keys.h"
|
||||
#include "network/system_name.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdlib>
|
||||
@@ -9,6 +10,7 @@
|
||||
|
||||
using namespace radiantwave::license;
|
||||
using namespace radiantwave::db;
|
||||
using namespace radiantwave::systemname;
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -103,6 +105,79 @@ TEST(LicenseServiceTest, GracePeriodIgnoresCorruptStamp) {
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
#include "network/system_name.h"
|
||||
#include "db/db_keys.h"
|
||||
#include "validation.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
using namespace radiantwave::systemname;
|
||||
using namespace radiantwave::db;
|
||||
namespace api = radiantwave::api;
|
||||
|
||||
namespace {
|
||||
|
||||
struct TempDb {
|
||||
TempDb() {
|
||||
path = (std::filesystem::temp_directory_path() /
|
||||
("test_rw_sysname_" + 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;
|
||||
|
||||
api::PutNameResult ok() { return api::PutNameResult{200, true}; }
|
||||
api::PutNameResult conflict() { return api::PutNameResult{409, true}; }
|
||||
api::PutNameResult invalid() { return api::PutNameResult{400, true}; }
|
||||
api::PutNameResult unknown() { return api::PutNameResult{404, true}; }
|
||||
api::PutNameResult offline() { return api::PutNameResult{0, false}; }
|
||||
|
||||
} // namespace
|
||||
|
||||
// ─── Validation ────────────────────────────────────────────────────────
|
||||
|
||||
TEST(SystemNameValidationTest, TrimAndBounds) {
|
||||
EXPECT_EQ(radiantwave::validation::trim(" Den "), "Den");
|
||||
EXPECT_TRUE(radiantwave::validation::isValidSystemName("Den"));
|
||||
EXPECT_TRUE(radiantwave::validation::isValidSystemName(" Den "));
|
||||
EXPECT_TRUE(radiantwave::validation::isValidSystemName(std::string(40, 'a')));
|
||||
EXPECT_FALSE(radiantwave::validation::isValidSystemName(""));
|
||||
EXPECT_FALSE(radiantwave::validation::isValidSystemName(" "));
|
||||
EXPECT_FALSE(radiantwave::validation::isValidSystemName(std::string(41, 'a')));
|
||||
}
|
||||
|
||||
// ─── Local storage ─────────────────────────────────────────────────────
|
||||
|
||||
TEST(SystemNameLocalTest, StoresTrimmedNameAndMarksPending) {
|
||||
TempDb tmp;
|
||||
std::string error;
|
||||
ASSERT_TRUE(storeLocalName(tmp.db, " Living Room ", &error));
|
||||
EXPECT_EQ(storedName(tmp.db), "Living Room");
|
||||
EXPECT_TRUE(pendingSync(tmp.db));
|
||||
}
|
||||
|
||||
TEST(SystemNameLocalTest, RejectsInvalidWithoutStoring) {
|
||||
TempDb tmp;
|
||||
std::string error;
|
||||
EXPECT_FALSE(storeLocalName(tmp.db, " ", &error));
|
||||
EXPECT_EQ(error, INVALID_MESSAGE);
|
||||
EXPECT_FALSE(tmp.db.hasKV(KV_SYSTEM_NAME));
|
||||
EXPECT_FALSE(pendingSync(tmp.db));
|
||||
|
||||
EXPECT_FALSE(storeLocalName(tmp.db, std::string(41, 'a')));
|
||||
EXPECT_FALSE(tmp.db.hasKV(KV_SYSTEM_NAME));
|
||||
}
|
||||
|
||||
// ─── Server status application (K2) ────────────────────────────────────
|
||||
|
||||
TEST(SystemNameStatusTest, OkMarksSyncedAndClearsPending) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
|
||||
|
||||
EXPECT_EQ(applyServerStatus(tmp.db, "ok"), Status::Ok);
|
||||
EXPECT_EQ(tmp.db.getKV(KV_SYSTEM_NAME_SYNCED), "Den");
|
||||
EXPECT_FALSE(pendingSync(tmp.db));
|
||||
}
|
||||
|
||||
TEST(SystemNameStatusTest, ConflictRevertsToSyncedAndRecordsMessage) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_SYNCED, "Living Room");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
|
||||
|
||||
EXPECT_EQ(applyServerStatus(tmp.db, "conflict"), Status::Conflict);
|
||||
EXPECT_EQ(storedName(tmp.db), "Living Room");
|
||||
EXPECT_FALSE(pendingSync(tmp.db));
|
||||
EXPECT_EQ(tmp.db.getKV(KV_SYSTEM_NAME_MESSAGE), CONFLICT_MESSAGE);
|
||||
}
|
||||
|
||||
TEST(SystemNameStatusTest, ConflictWithNoPreviousNameClearsIt) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
|
||||
|
||||
EXPECT_EQ(applyServerStatus(tmp.db, "conflict"), Status::Conflict);
|
||||
EXPECT_FALSE(tmp.db.hasKV(KV_SYSTEM_NAME));
|
||||
}
|
||||
|
||||
TEST(SystemNameStatusTest, InvalidRevertsAndRecordsMessage) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_SYNCED, "Den");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME, "Nope");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
|
||||
|
||||
EXPECT_EQ(applyServerStatus(tmp.db, "invalid"), Status::Invalid);
|
||||
EXPECT_EQ(storedName(tmp.db), "Den");
|
||||
EXPECT_EQ(tmp.db.getKV(KV_SYSTEM_NAME_MESSAGE), INVALID_MESSAGE);
|
||||
}
|
||||
|
||||
TEST(SystemNameStatusTest, UnchangedClearsPending) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
|
||||
|
||||
EXPECT_EQ(applyServerStatus(tmp.db, "unchanged"), Status::Unchanged);
|
||||
EXPECT_FALSE(pendingSync(tmp.db));
|
||||
}
|
||||
|
||||
// ─── Background sync (K3) ──────────────────────────────────────────────
|
||||
|
||||
TEST(SystemNameSyncTest, NotConfiguredWithoutLicenseKey) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
|
||||
|
||||
bool called = false;
|
||||
EXPECT_EQ(syncWithServer(tmp.db, [&](const std::string&, const std::string&) {
|
||||
called = true;
|
||||
return ok();
|
||||
}),
|
||||
Status::NotConfigured);
|
||||
EXPECT_FALSE(called);
|
||||
}
|
||||
|
||||
TEST(SystemNameSyncTest, DoesNothingWhenNotPending) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
|
||||
|
||||
bool called = false;
|
||||
EXPECT_EQ(syncWithServer(tmp.db, [&](const std::string&, const std::string&) {
|
||||
called = true;
|
||||
return ok();
|
||||
}),
|
||||
Status::Unchanged);
|
||||
EXPECT_FALSE(called);
|
||||
}
|
||||
|
||||
TEST(SystemNameSyncTest, PushesNameAndMarksSynced) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
|
||||
|
||||
std::string sentKey;
|
||||
std::string sentName;
|
||||
EXPECT_EQ(syncWithServer(tmp.db, [&](const std::string& k, const std::string& n) {
|
||||
sentKey = k;
|
||||
sentName = n;
|
||||
return ok();
|
||||
}),
|
||||
Status::Ok);
|
||||
EXPECT_EQ(sentKey, "ABCD-EFGH-IJKL-MNOP");
|
||||
EXPECT_EQ(sentName, "Den");
|
||||
EXPECT_FALSE(pendingSync(tmp.db));
|
||||
EXPECT_EQ(tmp.db.getKV(KV_SYSTEM_NAME_SYNCED), "Den");
|
||||
}
|
||||
|
||||
TEST(SystemNameSyncTest, ConflictRevertsAndKeepsOldName) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_SYNCED, "Living Room");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
|
||||
|
||||
EXPECT_EQ(syncWithServer(tmp.db, [](const std::string&, const std::string&) {
|
||||
return conflict();
|
||||
}),
|
||||
Status::Conflict);
|
||||
EXPECT_EQ(storedName(tmp.db), "Living Room");
|
||||
EXPECT_EQ(tmp.db.getKV(KV_SYSTEM_NAME_MESSAGE), CONFLICT_MESSAGE);
|
||||
}
|
||||
|
||||
TEST(SystemNameSyncTest, InvalidReverts) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_SYNCED, "Living Room");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME, "x");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
|
||||
|
||||
EXPECT_EQ(syncWithServer(tmp.db, [](const std::string&, const std::string&) {
|
||||
return invalid();
|
||||
}),
|
||||
Status::Invalid);
|
||||
EXPECT_EQ(storedName(tmp.db), "Living Room");
|
||||
}
|
||||
|
||||
TEST(SystemNameSyncTest, NetworkFailureKeepsPendingForRetry) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
|
||||
|
||||
EXPECT_EQ(syncWithServer(tmp.db, [](const std::string&, const std::string&) {
|
||||
return offline();
|
||||
}),
|
||||
Status::NetworkError);
|
||||
EXPECT_TRUE(pendingSync(tmp.db));
|
||||
EXPECT_EQ(storedName(tmp.db), "Den");
|
||||
}
|
||||
|
||||
TEST(SystemNameSyncTest, UnknownLicenseKeepsPending) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME, "Den");
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_PENDING, "1");
|
||||
|
||||
EXPECT_EQ(syncWithServer(tmp.db, [](const std::string&, const std::string&) {
|
||||
return unknown();
|
||||
}),
|
||||
Status::UnknownLicense);
|
||||
EXPECT_TRUE(pendingSync(tmp.db));
|
||||
}
|
||||
|
||||
TEST(SystemNameMessageTest, ConsumeReturnsAndClears) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_SYSTEM_NAME_MESSAGE, CONFLICT_MESSAGE);
|
||||
|
||||
EXPECT_EQ(consumeMessage(tmp.db), CONFLICT_MESSAGE);
|
||||
EXPECT_EQ(consumeMessage(tmp.db), "");
|
||||
}
|
||||
Reference in New Issue
Block a user