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:
@@ -59,6 +59,8 @@ add_executable(radiantwave
|
||||
src/network/api_client.cpp
|
||||
src/network/license_service.cpp
|
||||
src/network/license_status.cpp
|
||||
src/network/system_name.cpp
|
||||
src/network/system_name_sync.cpp
|
||||
src/config/yaml_parser.cpp
|
||||
src/config/config_manager.cpp
|
||||
src/db/database_manager.cpp
|
||||
@@ -193,9 +195,11 @@ target_link_libraries(test_api_client PRIVATE GTest::gtest_main CURL::libcurl SQ
|
||||
add_executable(test_license_service
|
||||
tests/test_license_service.cpp
|
||||
src/network/license_service.cpp
|
||||
src/network/system_name.cpp
|
||||
src/network/api_client.cpp
|
||||
src/network/json.cpp
|
||||
src/db/database_manager.cpp
|
||||
src/validation.cpp
|
||||
)
|
||||
target_include_directories(test_license_service PRIVATE src)
|
||||
target_link_libraries(test_license_service PRIVATE GTest::gtest_main CURL::libcurl SQLite3::SQLite3)
|
||||
@@ -205,9 +209,11 @@ add_executable(test_license_status
|
||||
tests/test_license_status.cpp
|
||||
src/network/license_status.cpp
|
||||
src/network/license_service.cpp
|
||||
src/network/system_name.cpp
|
||||
src/network/api_client.cpp
|
||||
src/network/json.cpp
|
||||
src/db/database_manager.cpp
|
||||
src/validation.cpp
|
||||
)
|
||||
target_include_directories(test_license_status PRIVATE src)
|
||||
target_link_libraries(test_license_status PRIVATE GTest::gtest_main CURL::libcurl SQLite3::SQLite3)
|
||||
@@ -236,6 +242,16 @@ add_executable(test_settings_menu
|
||||
target_include_directories(test_settings_menu PRIVATE src)
|
||||
target_link_libraries(test_settings_menu PRIVATE GTest::gtest_main)
|
||||
|
||||
# System Name local state + sync logic (HTTP layer is injected/faked)
|
||||
add_executable(test_system_name
|
||||
tests/test_system_name.cpp
|
||||
src/network/system_name.cpp
|
||||
src/validation.cpp
|
||||
src/db/database_manager.cpp
|
||||
)
|
||||
target_include_directories(test_system_name PRIVATE src)
|
||||
target_link_libraries(test_system_name PRIVATE GTest::gtest_main SQLite3::SQLite3)
|
||||
|
||||
# TimeZone unit tests
|
||||
add_executable(test_timezone
|
||||
tests/test_timezone.cpp
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "db/database_manager.h"
|
||||
#include "network/network_manager.h"
|
||||
#include "network/license_status.h"
|
||||
#include "network/system_name_sync.h"
|
||||
#include "audio/audio_manager.h"
|
||||
#include "audio/audio_downloader.h"
|
||||
|
||||
@@ -19,6 +20,7 @@ struct AppState {
|
||||
radiantwave::db::DatabaseManager db;
|
||||
radiantwave::network::NetworkManager network;
|
||||
radiantwave::license::LicenseStatusTracker licenseTracker;
|
||||
radiantwave::systemname::SystemNameSync systemNameSync;
|
||||
radiantwave::audio::AudioManager audio;
|
||||
radiantwave::audio::AudioDownloader audioDownloader;
|
||||
bool quitRequested = false;
|
||||
|
||||
@@ -12,4 +12,20 @@ constexpr const char* KV_LICENSE_KEY = "license_key";
|
||||
// outages do not revoke a known-good license.
|
||||
constexpr const char* KV_LICENSE_LAST_VERIFIED = "license_last_verified";
|
||||
|
||||
// The System Name the user typed on the Kiosk (trimmed). A human-readable label
|
||||
// set only here; never a reference or key.
|
||||
constexpr const char* KV_SYSTEM_NAME = "system_name";
|
||||
|
||||
// The last System Name the server accepted. Used to revert a local change when
|
||||
// the server reports a conflict or invalid value.
|
||||
constexpr const char* KV_SYSTEM_NAME_SYNCED = "system_name_synced";
|
||||
|
||||
// "1" while the local System Name still needs to be sent to the portal (e.g.
|
||||
// the license is not validated yet, or a previous sync failed).
|
||||
constexpr const char* KV_SYSTEM_NAME_PENDING = "system_name_pending_sync";
|
||||
|
||||
// The most recent System Name status message to show the user (e.g. a conflict
|
||||
// message after a background sync). Cleared once consumed/acknowledged.
|
||||
constexpr const char* KV_SYSTEM_NAME_MESSAGE = "system_name_message";
|
||||
|
||||
} // namespace radiantwave::db
|
||||
|
||||
@@ -162,6 +162,10 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) {
|
||||
// displayed; the result gates access to Play and the Proclamation options.
|
||||
state->licenseTracker.start(state->db);
|
||||
|
||||
// Background System Name sync. Retries a pending name on boot and on a
|
||||
// timer; license validation also applies the name (see license_service).
|
||||
state->systemNameSync.start(state->db);
|
||||
|
||||
state->config.onChanged(
|
||||
[state](const radiantwave::config::AppConfig &new_config) {
|
||||
SDL_Log("Live config update received: volume=%d, dynamic_audio=%d",
|
||||
@@ -271,6 +275,7 @@ void SDL_AppQuit(void *appstate, SDL_AppResult result) {
|
||||
auto *state = static_cast<AppState *>(appstate);
|
||||
if (state) {
|
||||
state->audioDownloader.shutdown();
|
||||
state->systemNameSync.shutdown();
|
||||
state->licenseTracker.shutdown();
|
||||
state->audio.shutdown();
|
||||
state->network.shutdown();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "network/json.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace radiantwave::api {
|
||||
@@ -26,9 +27,10 @@ size_t writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) {
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
// Shared curl setup + perform. Returns a populated HttpResponse.
|
||||
// Shared curl setup + perform. Returns a populated HttpResponse. `method` is
|
||||
// "GET", "POST", or "PUT"; GET sends no body.
|
||||
HttpResponse performRequest(const std::string& url,
|
||||
bool post, const std::string& jsonBody) {
|
||||
const char* method, const std::string& jsonBody) {
|
||||
HttpResponse response;
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
@@ -42,12 +44,17 @@ HttpResponse performRequest(const std::string& url,
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body);
|
||||
|
||||
const bool sendsBody = method != nullptr && std::string(method) != "GET";
|
||||
struct curl_slist* headers = nullptr;
|
||||
if (post) {
|
||||
if (sendsBody) {
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonBody.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(jsonBody.size()));
|
||||
if (std::string(method) == "POST") {
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
} else {
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method);
|
||||
}
|
||||
}
|
||||
if (headers) {
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
@@ -65,16 +72,44 @@ HttpResponse performRequest(const std::string& url,
|
||||
return response;
|
||||
}
|
||||
|
||||
// Escapes a string for inclusion in a JSON string literal.
|
||||
std::string jsonEscape(const std::string& input) {
|
||||
std::string out;
|
||||
out.reserve(input.size() + 2);
|
||||
for (unsigned char c : input) {
|
||||
switch (c) {
|
||||
case '"': out += "\\\""; break;
|
||||
case '\\': out += "\\\\"; break;
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': out += "\\r"; break;
|
||||
case '\t': out += "\\t"; break;
|
||||
default:
|
||||
if (c < 0x20) {
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
|
||||
out += buf;
|
||||
} else {
|
||||
out += static_cast<char>(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ─── HTTP helpers ──────────────────────────────────────────────────────
|
||||
|
||||
HttpResponse httpGet(const std::string& path) {
|
||||
return performRequest(urlFor(path), false, "");
|
||||
return performRequest(urlFor(path), "GET", "");
|
||||
}
|
||||
|
||||
HttpResponse httpPost(const std::string& path, const std::string& jsonBody) {
|
||||
return performRequest(urlFor(path), true, jsonBody);
|
||||
return performRequest(urlFor(path), "POST", jsonBody);
|
||||
}
|
||||
|
||||
HttpResponse httpPut(const std::string& path, const std::string& jsonBody) {
|
||||
return performRequest(urlFor(path), "PUT", jsonBody);
|
||||
}
|
||||
|
||||
// ─── GET /frequencies ──────────────────────────────────────────────────
|
||||
@@ -177,6 +212,7 @@ bool parseLicenseJson(const std::string& body, LicenseInfo& out) {
|
||||
|
||||
out.key = root.stringField("key");
|
||||
out.email_address = root.stringField("email_address");
|
||||
out.system_name_status = root.stringField("systemNameStatus");
|
||||
|
||||
// Any other fields (e.g. the legacy `subscription_level`) are ignored. A
|
||||
// successful validation itself is the "subscribed" flag; the kiosk treats
|
||||
@@ -185,11 +221,17 @@ bool parseLicenseJson(const std::string& body, LicenseInfo& out) {
|
||||
return !out.key.empty();
|
||||
}
|
||||
|
||||
LicenseResult validateLicense(const std::string& key, const std::string& email) {
|
||||
LicenseResult validateLicense(const std::string& key, const std::string& email,
|
||||
const std::string& systemName) {
|
||||
LicenseResult result;
|
||||
|
||||
const std::string jsonBody =
|
||||
"{\"license_key\":\"" + key + "\",\"email_address\":\"" + email + "\"}";
|
||||
std::string jsonBody = "{\"license_key\":\"" + jsonEscape(key) +
|
||||
"\",\"email_address\":\"" + jsonEscape(email) + "\"";
|
||||
if (!systemName.empty()) {
|
||||
jsonBody += ",\"systemName\":\"" + jsonEscape(systemName) + "\"";
|
||||
}
|
||||
jsonBody += "}";
|
||||
|
||||
HttpResponse response = httpPost(PATH_LICENSE, jsonBody);
|
||||
result.status = response.status;
|
||||
|
||||
@@ -327,4 +369,18 @@ AccountResult createAccount(const std::string& email, const std::string& name) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── PUT /licenses/{key}/system-name ───────────────────────────────────
|
||||
|
||||
PutNameResult putSystemName(const std::string& key, const std::string& name) {
|
||||
PutNameResult result;
|
||||
|
||||
const std::string path = "/licenses/" + urlEncode(key) + "/system-name";
|
||||
const std::string jsonBody = "{\"systemName\":\"" + jsonEscape(name) + "\"}";
|
||||
|
||||
HttpResponse response = httpPut(path, jsonBody);
|
||||
result.status = static_cast<int>(response.status);
|
||||
result.transport_ok = response.transportOk;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace radiantwave::api
|
||||
|
||||
@@ -26,9 +26,10 @@ struct HttpResponse {
|
||||
bool transportOk = false; // true when the request completed without a transport error
|
||||
};
|
||||
|
||||
// Performs a GET/POST against `path` (relative to API_BASE_URL).
|
||||
// Performs a GET/POST/PUT against `path` (relative to API_BASE_URL).
|
||||
HttpResponse httpGet(const std::string& path);
|
||||
HttpResponse httpPost(const std::string& path, const std::string& jsonBody);
|
||||
HttpResponse httpPut(const std::string& path, const std::string& jsonBody);
|
||||
|
||||
// ─── GET /frequencies ──────────────────────────────────────────────────
|
||||
|
||||
@@ -79,6 +80,10 @@ bool fetchColorPalettes(std::vector<ColorPalette>& out);
|
||||
struct LicenseInfo {
|
||||
std::string key;
|
||||
std::string email_address;
|
||||
// Advisory System Name status returned by the portal: one of "ok",
|
||||
// "unchanged", "conflict", "invalid", or empty when the portal did not
|
||||
// report one (e.g. older portal).
|
||||
std::string system_name_status;
|
||||
};
|
||||
|
||||
// Parse the 200 JSON payload returned by the license endpoint.
|
||||
@@ -95,7 +100,25 @@ struct LicenseResult {
|
||||
// Validate a license key / email pair against the portal. Never throws; failures
|
||||
// (network, HTTP error status) are reported through the returned LicenseResult.
|
||||
// A 2xx response means the license is valid and the subscription is active.
|
||||
LicenseResult validateLicense(const std::string& key, const std::string& email);
|
||||
// When `systemName` is non-empty it is sent as the additive `systemName` field
|
||||
// (the Kiosk is the only writer); the response's `systemNameStatus` is advisory.
|
||||
LicenseResult validateLicense(const std::string& key, const std::string& email,
|
||||
const std::string& systemName = "");
|
||||
|
||||
// ─── PUT /licenses/{key}/system-name ───────────────────────────────────
|
||||
|
||||
// Result of a System Name PUT. `status` is the HTTP status (0 on transport
|
||||
// failure); `transport_ok` is false when the request never completed.
|
||||
struct PutNameResult {
|
||||
int status = 0;
|
||||
bool transport_ok = false;
|
||||
|
||||
bool ok() const { return status >= 200 && status < 300; }
|
||||
};
|
||||
|
||||
// Set the System Name for a License Key. 200 on success, 400 invalid_name,
|
||||
// 404 unknown_license, 409 name_taken. Never throws.
|
||||
PutNameResult putSystemName(const std::string& key, const std::string& name);
|
||||
|
||||
// ─── POST /automations/create/account ──────────────────────────────────
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "network/license_service.h"
|
||||
#include "db/db_keys.h"
|
||||
#include "network/api_client.h"
|
||||
#include "network/system_name.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
@@ -18,6 +19,18 @@ long long nowEpochSeconds() {
|
||||
|
||||
Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
|
||||
std::string* outErrorMessage) {
|
||||
return validateStoredLicenseWith(
|
||||
db,
|
||||
[](const std::string& key, const std::string& email,
|
||||
const std::string& systemName) {
|
||||
return radiantwave::api::validateLicense(key, email, systemName);
|
||||
},
|
||||
outErrorMessage);
|
||||
}
|
||||
|
||||
Result validateStoredLicenseWith(radiantwave::db::DatabaseManager& db,
|
||||
const ValidateFn& validate,
|
||||
std::string* outErrorMessage) {
|
||||
if (outErrorMessage) outErrorMessage->clear();
|
||||
|
||||
const std::string email = db.getKV(radiantwave::db::KV_EMAIL_ADDRESS);
|
||||
@@ -26,8 +39,10 @@ Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
|
||||
return Result::NotConfigured;
|
||||
}
|
||||
|
||||
const radiantwave::api::LicenseResult result =
|
||||
radiantwave::api::validateLicense(key, email);
|
||||
// The Kiosk is the only writer of the System Name; send the locally stored
|
||||
// name (possibly empty) so the portal can confirm or reject it.
|
||||
const std::string systemName = radiantwave::systemname::storedName(db);
|
||||
const radiantwave::api::LicenseResult result = validate(key, email, systemName);
|
||||
|
||||
if (result.status == 0) {
|
||||
if (outErrorMessage) *outErrorMessage = "network error";
|
||||
@@ -42,6 +57,12 @@ Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
|
||||
// and the subscription is active. Only the "last verified" stamp is stored
|
||||
// (it drives the offline grace window).
|
||||
markLicenseVerified(db);
|
||||
|
||||
// Apply the advisory System Name status. A conflict/invalid reverts the
|
||||
// local name and records a message the UI can surface.
|
||||
if (!result.info.system_name_status.empty()) {
|
||||
radiantwave::systemname::applyServerStatus(db, result.info.system_name_status);
|
||||
}
|
||||
return Result::Ok;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "db/database_manager.h"
|
||||
#include "network/api_client.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace radiantwave::license {
|
||||
@@ -28,6 +30,16 @@ constexpr long long GRACE_PERIOD_SECONDS = 7LL * 24 * 60 * 60;
|
||||
Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
|
||||
std::string* outErrorMessage = nullptr);
|
||||
|
||||
// Injectable core of validateStoredLicense: performs the given validation call
|
||||
// instead of the live portal. On success it stamps "last verified" and applies
|
||||
// any advisory `systemNameStatus` to the local System Name state.
|
||||
using ValidateFn = std::function<api::LicenseResult(
|
||||
const std::string& key, const std::string& email, const std::string& systemName)>;
|
||||
|
||||
Result validateStoredLicenseWith(radiantwave::db::DatabaseManager& db,
|
||||
const ValidateFn& validate,
|
||||
std::string* outErrorMessage = nullptr);
|
||||
|
||||
// Records the current time as the "last verified" stamp of the license. Called
|
||||
// after every successful server validation so the offline grace window starts
|
||||
// (or is extended) from the most recent confirmation.
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "network/system_name.h"
|
||||
#include "db/db_keys.h"
|
||||
#include "validation.h"
|
||||
|
||||
namespace radiantwave::systemname {
|
||||
|
||||
namespace {
|
||||
|
||||
void revertToSynced(db::DatabaseManager& db) {
|
||||
const std::string synced = db.getKV(db::KV_SYSTEM_NAME_SYNCED);
|
||||
if (synced.empty()) {
|
||||
db.deleteKV(db::KV_SYSTEM_NAME);
|
||||
} else {
|
||||
db.setKV(db::KV_SYSTEM_NAME, synced);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status parseStatus(const std::string& status) {
|
||||
if (status == "ok") return Status::Ok;
|
||||
if (status == "conflict") return Status::Conflict;
|
||||
if (status == "invalid") return Status::Invalid;
|
||||
return Status::Unchanged;
|
||||
}
|
||||
|
||||
std::string storedName(const db::DatabaseManager& db) {
|
||||
return db.getKV(db::KV_SYSTEM_NAME);
|
||||
}
|
||||
|
||||
bool pendingSync(const db::DatabaseManager& db) {
|
||||
return db.getKV(db::KV_SYSTEM_NAME_PENDING) == "1";
|
||||
}
|
||||
|
||||
bool storeLocalName(db::DatabaseManager& db, const std::string& raw,
|
||||
std::string* error) {
|
||||
const std::string trimmed = validation::trim(raw);
|
||||
if (!validation::isValidSystemName(trimmed)) {
|
||||
if (error) *error = INVALID_MESSAGE;
|
||||
return false;
|
||||
}
|
||||
|
||||
db.setKV(db::KV_SYSTEM_NAME, trimmed);
|
||||
db.setKV(db::KV_SYSTEM_NAME_PENDING, "1");
|
||||
db.setKV(db::KV_SYSTEM_NAME_MESSAGE, "");
|
||||
return true;
|
||||
}
|
||||
|
||||
Status applyServerStatus(db::DatabaseManager& db, const std::string& serverStatus) {
|
||||
const Status status = parseStatus(serverStatus);
|
||||
switch (status) {
|
||||
case Status::Ok:
|
||||
db.setKV(db::KV_SYSTEM_NAME_SYNCED, db.getKV(db::KV_SYSTEM_NAME));
|
||||
db.setKV(db::KV_SYSTEM_NAME_PENDING, "0");
|
||||
db.setKV(db::KV_SYSTEM_NAME_MESSAGE, "");
|
||||
return Status::Ok;
|
||||
case Status::Unchanged:
|
||||
if (!db.getKV(db::KV_SYSTEM_NAME).empty()) {
|
||||
db.setKV(db::KV_SYSTEM_NAME_SYNCED, db.getKV(db::KV_SYSTEM_NAME));
|
||||
}
|
||||
db.setKV(db::KV_SYSTEM_NAME_PENDING, "0");
|
||||
db.setKV(db::KV_SYSTEM_NAME_MESSAGE, "");
|
||||
return Status::Unchanged;
|
||||
case Status::Conflict:
|
||||
revertToSynced(db);
|
||||
db.setKV(db::KV_SYSTEM_NAME_PENDING, "0");
|
||||
db.setKV(db::KV_SYSTEM_NAME_MESSAGE, CONFLICT_MESSAGE);
|
||||
return Status::Conflict;
|
||||
case Status::Invalid:
|
||||
revertToSynced(db);
|
||||
db.setKV(db::KV_SYSTEM_NAME_PENDING, "0");
|
||||
db.setKV(db::KV_SYSTEM_NAME_MESSAGE, INVALID_MESSAGE);
|
||||
return Status::Invalid;
|
||||
default:
|
||||
return Status::Unchanged;
|
||||
}
|
||||
}
|
||||
|
||||
Status syncWithServer(db::DatabaseManager& db, const PutFn& put) {
|
||||
const std::string key = db.getKV(db::KV_LICENSE_KEY);
|
||||
if (key.empty()) {
|
||||
return Status::NotConfigured;
|
||||
}
|
||||
|
||||
const std::string name = db.getKV(db::KV_SYSTEM_NAME);
|
||||
if (name.empty()) {
|
||||
// Nothing to sync; a cleared/empty name is never sent.
|
||||
db.setKV(db::KV_SYSTEM_NAME_PENDING, "0");
|
||||
return Status::Unchanged;
|
||||
}
|
||||
|
||||
if (!pendingSync(db)) {
|
||||
return Status::Unchanged;
|
||||
}
|
||||
|
||||
const api::PutNameResult result = put(key, name);
|
||||
if (!result.transport_ok) {
|
||||
return Status::NetworkError; // keep pending for a later retry
|
||||
}
|
||||
|
||||
switch (result.status) {
|
||||
case 200:
|
||||
return applyServerStatus(db, "ok");
|
||||
case 400:
|
||||
return applyServerStatus(db, "invalid");
|
||||
case 409:
|
||||
return applyServerStatus(db, "conflict");
|
||||
case 404:
|
||||
return Status::UnknownLicense; // keep pending; license not validated
|
||||
default:
|
||||
return Status::NetworkError; // 5xx etc: keep pending
|
||||
}
|
||||
}
|
||||
|
||||
std::string consumeMessage(db::DatabaseManager& db) {
|
||||
const std::string message = db.getKV(db::KV_SYSTEM_NAME_MESSAGE);
|
||||
if (!message.empty()) {
|
||||
db.setKV(db::KV_SYSTEM_NAME_MESSAGE, "");
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
} // namespace radiantwave::systemname
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include "db/database_manager.h"
|
||||
#include "network/api_client.h"
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace radiantwave::systemname {
|
||||
|
||||
// Outcome of a System Name operation.
|
||||
enum class Status {
|
||||
Ok, // server accepted the name (or it was saved locally)
|
||||
Unchanged, // no change needed / already in sync
|
||||
Conflict, // another Kiosk on the account already uses the name
|
||||
Invalid, // 1-40 character validation failed
|
||||
NetworkError, // the request never completed; safe to retry
|
||||
UnknownLicense,// the portal does not know this License Key
|
||||
NotConfigured, // no License Key stored yet; nothing to sync
|
||||
};
|
||||
|
||||
// Sends a System Name to the portal. Injectable so tests can fake the HTTP
|
||||
// layer. The production default is `api::putSystemName`.
|
||||
using PutFn = std::function<api::PutNameResult(const std::string& key,
|
||||
const std::string& name)>;
|
||||
|
||||
// Messages shown to the user. Kept here so the page and the service agree.
|
||||
constexpr const char* CONFLICT_MESSAGE =
|
||||
"That name is already used on your account. Choose another.";
|
||||
constexpr const char* INVALID_MESSAGE =
|
||||
"Enter a name between 1 and 40 characters.";
|
||||
|
||||
// Maps a portal `systemNameStatus` string ("ok"/"unchanged"/"conflict"/
|
||||
// "invalid") to a Status. Unknown/empty maps to Unchanged.
|
||||
Status parseStatus(const std::string& status);
|
||||
|
||||
// The locally stored System Name (may be empty).
|
||||
std::string storedName(const db::DatabaseManager& db);
|
||||
|
||||
// True while the stored name still needs to be sent to the portal.
|
||||
bool pendingSync(const db::DatabaseManager& db);
|
||||
|
||||
// Validates and persists `raw` locally (trimmed), marks it pending sync, and
|
||||
// clears any stale message. Returns false and sets `error` when invalid. The
|
||||
// caller is responsible for triggering a sync; the UI is never blocked.
|
||||
bool storeLocalName(db::DatabaseManager& db, const std::string& raw,
|
||||
std::string* error = nullptr);
|
||||
|
||||
// Applies a portal `systemNameStatus` to local state. On "ok"/"unchanged" the
|
||||
// name is marked synced; on "conflict"/"invalid" the local name is reverted to
|
||||
// the last server-accepted value and a user-facing message is stored.
|
||||
Status applyServerStatus(db::DatabaseManager& db, const std::string& serverStatus);
|
||||
|
||||
// If a sync is pending and a License Key is stored, PUTs the name via `put` and
|
||||
// applies the result. Network failures and 404s leave the pending flag set so
|
||||
// the caller can retry later.
|
||||
Status syncWithServer(db::DatabaseManager& db, const PutFn& put);
|
||||
|
||||
// Returns the pending user-facing message and clears it (consume-once).
|
||||
std::string consumeMessage(db::DatabaseManager& db);
|
||||
|
||||
} // namespace radiantwave::systemname
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "network/system_name_sync.h"
|
||||
#include "network/api_client.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace radiantwave::systemname {
|
||||
|
||||
SystemNameSync::SystemNameSync()
|
||||
: put_([](const std::string& key, const std::string& name) {
|
||||
return api::putSystemName(key, name);
|
||||
}) {
|
||||
}
|
||||
|
||||
SystemNameSync::SystemNameSync(PutFn put) : put_(std::move(put)) {
|
||||
}
|
||||
|
||||
SystemNameSync::~SystemNameSync() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
void SystemNameSync::start(db::DatabaseManager& db) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (worker_.joinable()) return;
|
||||
db_ = &db;
|
||||
stop_.store(false);
|
||||
worker_ = std::thread(&SystemNameSync::run, this);
|
||||
requested_.store(true);
|
||||
cv_.notify_one();
|
||||
}
|
||||
|
||||
void SystemNameSync::shutdown() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
stop_.store(true);
|
||||
}
|
||||
cv_.notify_one();
|
||||
if (worker_.joinable()) {
|
||||
worker_.join();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemNameSync::requestSync() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!worker_.joinable()) return;
|
||||
requested_.store(true);
|
||||
}
|
||||
cv_.notify_one();
|
||||
}
|
||||
|
||||
void SystemNameSync::run() {
|
||||
while (true) {
|
||||
db::DatabaseManager* db = nullptr;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
cv_.wait_for(lock, RETRY_INTERVAL, [this] {
|
||||
return stop_.load() || requested_.load();
|
||||
});
|
||||
if (stop_.load()) return;
|
||||
requested_.store(false);
|
||||
db = db_;
|
||||
}
|
||||
|
||||
if (db && pendingSync(*db)) {
|
||||
syncWithServer(*db, put_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace radiantwave::systemname
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "db/database_manager.h"
|
||||
#include "network/system_name.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace radiantwave::systemname {
|
||||
|
||||
// Background worker that pushes a pending System Name to the portal without
|
||||
// blocking the UI. It retries on a timer and can be nudged immediately
|
||||
// (`requestSync`) after a local change. `start()` attempts one sync right away,
|
||||
// covering the retry-on-boot case; a successful license validation also syncs
|
||||
// the name (see license_service), covering the retry-on-validation case.
|
||||
class SystemNameSync {
|
||||
public:
|
||||
SystemNameSync();
|
||||
explicit SystemNameSync(PutFn put);
|
||||
~SystemNameSync();
|
||||
|
||||
SystemNameSync(const SystemNameSync&) = delete;
|
||||
SystemNameSync& operator=(const SystemNameSync&) = delete;
|
||||
|
||||
void start(db::DatabaseManager& db);
|
||||
void shutdown();
|
||||
void requestSync();
|
||||
|
||||
// Retry interval for the periodic attempt.
|
||||
static constexpr std::chrono::seconds RETRY_INTERVAL{30};
|
||||
|
||||
private:
|
||||
void run();
|
||||
|
||||
PutFn put_;
|
||||
db::DatabaseManager* db_ = nullptr;
|
||||
std::thread worker_;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable cv_;
|
||||
std::atomic<bool> stop_{false};
|
||||
std::atomic<bool> requested_{false};
|
||||
};
|
||||
|
||||
} // namespace radiantwave::systemname
|
||||
@@ -87,4 +87,21 @@ std::string formatLicenseKey(const std::string& sanitized) {
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string trim(const std::string& input) {
|
||||
size_t start = 0;
|
||||
size_t end = input.size();
|
||||
while (start < end && std::isspace(static_cast<unsigned char>(input[start]))) {
|
||||
++start;
|
||||
}
|
||||
while (end > start && std::isspace(static_cast<unsigned char>(input[end - 1]))) {
|
||||
--end;
|
||||
}
|
||||
return input.substr(start, end - start);
|
||||
}
|
||||
|
||||
bool isValidSystemName(const std::string& name) {
|
||||
const std::string trimmed = trim(name);
|
||||
return !trimmed.empty() && trimmed.size() <= SYSTEM_NAME_MAX_LENGTH;
|
||||
}
|
||||
|
||||
} // namespace radiantwave::validation
|
||||
|
||||
@@ -17,4 +17,14 @@ std::string sanitizeLicenseKey(const std::string& input);
|
||||
// characters (XXXX-XXXX-XXXX-XXXX). Partial input yields partial output.
|
||||
std::string formatLicenseKey(const std::string& sanitized);
|
||||
|
||||
// The maximum length of a System Name (per the API contract).
|
||||
constexpr size_t SYSTEM_NAME_MAX_LENGTH = 40;
|
||||
|
||||
// Removes leading and trailing ASCII whitespace from `input`.
|
||||
std::string trim(const std::string& input);
|
||||
|
||||
// Returns true when `name` (already trimmed) is a valid System Name: 1-40
|
||||
// characters. Empty or whitespace-only names are invalid (never a clear).
|
||||
bool isValidSystemName(const std::string& name);
|
||||
|
||||
} // namespace radiantwave::validation
|
||||
|
||||
@@ -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