initial commit

This commit is contained in:
NeilClack
2026-08-07 14:51:41 -05:00
commit 9251b3a196
604 changed files with 464448 additions and 0 deletions
+380
View File
@@ -0,0 +1,380 @@
#include "network/nm_connection.h"
#include "network/nm_types.h"
#include <gtest/gtest.h>
#include <stdexcept>
#include <string>
using namespace radiantwave::network;
// ─── Open Network Tests ────────────────────────────────────────────────
TEST(ConnectionBuilderTest, OpenNetworkNoSecurity) {
auto settings = buildConnectionSettings("MyWiFi", "", SecurityType::Open);
// Should have connection, wireless, ipv4, ipv6 sections
EXPECT_EQ(settings.count("connection"), 1u);
EXPECT_EQ(settings.count("802-11-wireless"), 1u);
EXPECT_EQ(settings.count("ipv4"), 1u);
EXPECT_EQ(settings.count("ipv6"), 1u);
// Should NOT have security section
EXPECT_EQ(settings.count("802-11-wireless-security"), 0u);
}
TEST(ConnectionBuilderTest, OpenNetworkConnectionId) {
auto settings = buildConnectionSettings("MyWiFi", "", SecurityType::Open);
auto& conn = settings["connection"];
EXPECT_EQ(std::get<std::string>(conn["id"]), "MyWiFi");
EXPECT_EQ(std::get<std::string>(conn["type"]), "802-11-wireless");
EXPECT_EQ(std::get<bool>(conn["autoconnect"]), true);
}
TEST(ConnectionBuilderTest, OpenNetworkWirelessMode) {
auto settings = buildConnectionSettings("MyWiFi", "", SecurityType::Open);
auto& wireless = settings["802-11-wireless"];
EXPECT_EQ(std::get<std::string>(wireless["mode"]), "infrastructure");
}
TEST(ConnectionBuilderTest, OpenNetworkIpSettings) {
auto settings = buildConnectionSettings("MyWiFi", "", SecurityType::Open);
EXPECT_EQ(std::get<std::string>(settings["ipv4"]["method"]), "auto");
EXPECT_EQ(std::get<std::string>(settings["ipv6"]["method"]), "auto");
}
// ─── SSID Handling Tests ───────────────────────────────────────────────
TEST(ConnectionBuilderTest, SSIDIsByteArray) {
auto settings = buildConnectionSettings("MyWiFi", "", SecurityType::Open);
auto& wireless = settings["802-11-wireless"];
auto ssid = std::get<std::vector<uint8_t>>(wireless["ssid"]);
EXPECT_EQ(ssid.size(), 6u);
EXPECT_EQ(std::string(ssid.begin(), ssid.end()), "MyWiFi");
}
TEST(ConnectionBuilderTest, SSIDWithSpaces) {
auto settings = buildConnectionSettings("My WiFi Network", "", SecurityType::Open);
auto& wireless = settings["802-11-wireless"];
auto ssid = std::get<std::vector<uint8_t>>(wireless["ssid"]);
EXPECT_EQ(std::string(ssid.begin(), ssid.end()), "My WiFi Network");
}
TEST(ConnectionBuilderTest, SSIDWithUnicode) {
auto settings = buildConnectionSettings("Café WiFi", "", SecurityType::Open);
auto& wireless = settings["802-11-wireless"];
auto ssid = std::get<std::vector<uint8_t>>(wireless["ssid"]);
std::string ssid_str(ssid.begin(), ssid.end());
EXPECT_EQ(ssid_str, "Café WiFi");
}
TEST(ConnectionBuilderTest, SSIDWithJapanese) {
auto settings = buildConnectionSettings("日本語ネットワーク", "", SecurityType::Open);
auto& wireless = settings["802-11-wireless"];
auto ssid = std::get<std::vector<uint8_t>>(wireless["ssid"]);
std::string ssid_str(ssid.begin(), ssid.end());
EXPECT_EQ(ssid_str, "日本語ネットワーク");
}
TEST(ConnectionBuilderTest, EmptySSIDFails) {
EXPECT_THROW(
buildConnectionSettings("", "", SecurityType::Open),
std::invalid_argument
);
}
TEST(ConnectionBuilderTest, SSIDTooLongFails) {
// SSIDs max 32 bytes
std::string long_ssid(33, 'A');
EXPECT_THROW(
buildConnectionSettings(long_ssid, "", SecurityType::Open),
std::invalid_argument
);
}
TEST(ConnectionBuilderTest, SSIDWithNullByteFails) {
EXPECT_THROW(
buildConnectionSettings(std::string("My\0WiFi", 7), "", SecurityType::Open),
std::invalid_argument
);
}
// ─── WPA2 Tests ────────────────────────────────────────────────────────
TEST(ConnectionBuilderTest, WPA2HasSecuritySection) {
auto settings = buildConnectionSettings("SecureNet", "password123", SecurityType::WPA2);
EXPECT_EQ(settings.count("802-11-wireless-security"), 1u);
}
TEST(ConnectionBuilderTest, WPA2KeyMgmt) {
auto settings = buildConnectionSettings("SecureNet", "password123", SecurityType::WPA2);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["key-mgmt"]), "wpa-psk");
EXPECT_EQ(std::get<std::string>(sec["auth-alg"]), "open");
}
TEST(ConnectionBuilderTest, WPA2Password) {
auto settings = buildConnectionSettings("SecureNet", "password123", SecurityType::WPA2);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["psk"]), "password123");
}
TEST(ConnectionBuilderTest, WPA2EmptyPasswordFails) {
EXPECT_THROW(
buildConnectionSettings("SecureNet", "", SecurityType::WPA2),
std::invalid_argument
);
}
TEST(ConnectionBuilderTest, WPA2ShortPasswordFails) {
EXPECT_THROW(
buildConnectionSettings("SecureNet", "short", SecurityType::WPA2),
std::invalid_argument
);
}
TEST(ConnectionBuilderTest, WPA2LongPasswordFails) {
// Max 63 characters
std::string long_pass(64, 'a');
EXPECT_THROW(
buildConnectionSettings("SecureNet", long_pass, SecurityType::WPA2),
std::invalid_argument
);
}
TEST(ConnectionBuilderTest, WPA2MaxPassword) {
std::string max_pass(63, 'a');
EXPECT_NO_THROW(
buildConnectionSettings("SecureNet", max_pass, SecurityType::WPA2)
);
}
// ─── WPA3 Tests ────────────────────────────────────────────────────────
TEST(ConnectionBuilderTest, WPA3KeyMgmt) {
auto settings = buildConnectionSettings("SecureNet3", "password123", SecurityType::WPA3);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["key-mgmt"]), "sae");
}
TEST(ConnectionBuilderTest, WPA3Password) {
auto settings = buildConnectionSettings("SecureNet3", "password123", SecurityType::WPA3);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["psk"]), "password123");
}
TEST(ConnectionBuilderTest, WPA3EmptyPasswordFails) {
EXPECT_THROW(
buildConnectionSettings("SecureNet3", "", SecurityType::WPA3),
std::invalid_argument
);
}
// ─── WEP Tests ─────────────────────────────────────────────────────────
TEST(ConnectionBuilderTest, WEPKeyMgmt) {
auto settings = buildConnectionSettings("OldNet", "weppass", SecurityType::WEP);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["key-mgmt"]), "none");
}
TEST(ConnectionBuilderTest, WEPPassword) {
auto settings = buildConnectionSettings("OldNet", "weppass", SecurityType::WEP);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["wep-key0"]), "weppass");
}
TEST(ConnectionBuilderTest, WEPKeyType) {
auto settings = buildConnectionSettings("OldNet", "weppass", SecurityType::WEP);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<uint32_t>(sec["wep-key-type"]), 1u);
}
TEST(ConnectionBuilderTest, WEPEmptyPasswordFails) {
EXPECT_THROW(
buildConnectionSettings("OldNet", "", SecurityType::WEP),
std::invalid_argument
);
}
// ─── Special Character Tests ───────────────────────────────────────────
TEST(ConnectionBuilderTest, PasswordWithSingleQuote) {
auto settings = buildConnectionSettings("Net", "p@ss'w0rd", SecurityType::WPA2);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["psk"]), "p@ss'w0rd");
}
TEST(ConnectionBuilderTest, PasswordWithDoubleQuote) {
auto settings = buildConnectionSettings("Net", R"(p@ss"w0rd)", SecurityType::WPA2);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["psk"]), R"(p@ss"w0rd)");
}
TEST(ConnectionBuilderTest, PasswordWithAt) {
auto settings = buildConnectionSettings("Net", "user@domain.com", SecurityType::WPA2);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["psk"]), "user@domain.com");
}
TEST(ConnectionBuilderTest, PasswordWithSpecialChars) {
std::string special = "!@#$%^&*()_+-=[]{}|;':\",./<>?`~";
auto settings = buildConnectionSettings("Net", special, SecurityType::WPA2);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["psk"]), special);
}
TEST(ConnectionBuilderTest, PasswordWithSpaces) {
auto settings = buildConnectionSettings("Net", "password with spaces", SecurityType::WPA2);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["psk"]), "password with spaces");
}
TEST(ConnectionBuilderTest, PasswordWithUnicode) {
auto settings = buildConnectionSettings("Net", "pässwörd", SecurityType::WPA2);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["psk"]), "pässwörd");
}
TEST(ConnectionBuilderTest, PasswordWithEmoji) {
auto settings = buildConnectionSettings("Net", "🔐🔑password", SecurityType::WPA2);
auto& sec = settings["802-11-wireless-security"];
EXPECT_EQ(std::get<std::string>(sec["psk"]), "🔐🔑password");
}
// ─── Security Detection Tests ──────────────────────────────────────────
TEST(SecurityDetectionTest, OpenNetwork) {
EXPECT_EQ(detectSecurityType(0, 0, 0), SecurityType::Open);
}
TEST(SecurityDetectionTest, WEPWithPrivacy) {
EXPECT_EQ(detectSecurityType(0x01, 0, 0), SecurityType::WEP);
}
TEST(SecurityDetectionTest, WPAWithWpaFlags) {
EXPECT_EQ(detectSecurityType(0, 0x01, 0), SecurityType::WPA);
}
TEST(SecurityDetectionTest, WPA2WithRsnPsk) {
EXPECT_EQ(detectSecurityType(0, 0, 0x100), SecurityType::WPA2);
}
TEST(SecurityDetectionTest, WPA3WithSae) {
EXPECT_EQ(detectSecurityType(0, 0, 0x400), SecurityType::WPA3);
}
TEST(SecurityDetectionTest, WPA3TakesPrecedenceOverWPA2) {
// SAE (0x400) should be detected before PSK (0x100)
EXPECT_EQ(detectSecurityType(0, 0, 0x500), SecurityType::WPA3);
}
TEST(SecurityDetectionTest, WPA2TakesPrecedenceOverWPA) {
// RSN PSK should be detected before WPA flags
EXPECT_EQ(detectSecurityType(0, 0x01, 0x100), SecurityType::WPA2);
}
// ─── SSID Utility Tests ────────────────────────────────────────────────
TEST(SsidTest, SsidToBytesRoundTrip) {
std::string ssid = "MyWiFi";
auto bytes = ssidToBytes(ssid);
auto result = bytesToSsid(bytes);
EXPECT_EQ(result, ssid);
}
TEST(SsidTest, SsidToBytesUnicode) {
std::string ssid = "Café";
auto bytes = ssidToBytes(ssid);
auto result = bytesToSsid(bytes);
EXPECT_EQ(result, ssid);
}
TEST(SsidTest, IsValidSsidNormal) {
EXPECT_TRUE(isValidSsid("MyWiFi"));
EXPECT_TRUE(isValidSsid("A"));
EXPECT_TRUE(isValidSsid(std::string(32, 'A')));
}
TEST(SsidTest, IsValidSsidEmpty) {
EXPECT_FALSE(isValidSsid(""));
}
TEST(SsidTest, IsValidSsidTooLong) {
EXPECT_FALSE(isValidSsid(std::string(33, 'A')));
}
TEST(SsidTest, IsValidSsidWithNull) {
EXPECT_FALSE(isValidSsid(std::string("My\0WiFi", 7)));
}
// ─── Helper Function Tests ─────────────────────────────────────────────
TEST(HelperTest, RequiresPassword) {
EXPECT_FALSE(requiresPassword(SecurityType::Open));
EXPECT_TRUE(requiresPassword(SecurityType::WEP));
EXPECT_TRUE(requiresPassword(SecurityType::WPA));
EXPECT_TRUE(requiresPassword(SecurityType::WPA2));
EXPECT_TRUE(requiresPassword(SecurityType::WPA3));
}
TEST(HelperTest, SecurityTypeToString) {
EXPECT_STREQ(securityTypeToString(SecurityType::Open), "Open");
EXPECT_STREQ(securityTypeToString(SecurityType::WEP), "WEP");
EXPECT_STREQ(securityTypeToString(SecurityType::WPA), "WPA");
EXPECT_STREQ(securityTypeToString(SecurityType::WPA2), "WPA2");
EXPECT_STREQ(securityTypeToString(SecurityType::WPA3), "WPA3");
}
TEST(HelperTest, ConnectionStateToString) {
EXPECT_STREQ(connectionStateToString(ConnectionState::Disconnected), "Disconnected");
EXPECT_STREQ(connectionStateToString(ConnectionState::Activated), "Connected");
EXPECT_STREQ(connectionStateToString(ConnectionState::Failed), "Failed");
EXPECT_STREQ(connectionStateToString(ConnectionState::NeedAuth), "Authentication Required");
}
// ─── UUID Generation Tests ─────────────────────────────────────────────
TEST(UuidTest, UUIDFormat) {
auto settings = buildConnectionSettings("Net", "", SecurityType::Open);
auto uuid = std::get<std::string>(settings["connection"]["uuid"]);
// UUID format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
EXPECT_EQ(uuid.size(), 36u);
EXPECT_EQ(uuid[8], '-');
EXPECT_EQ(uuid[13], '-');
EXPECT_EQ(uuid[18], '-');
EXPECT_EQ(uuid[23], '-');
EXPECT_EQ(uuid[14], '4'); // Version 4
}
TEST(UuidTest, UniqueUUIDs) {
auto settings1 = buildConnectionSettings("Net1", "", SecurityType::Open);
auto settings2 = buildConnectionSettings("Net2", "", SecurityType::Open);
auto uuid1 = std::get<std::string>(settings1["connection"]["uuid"]);
auto uuid2 = std::get<std::string>(settings2["connection"]["uuid"]);
EXPECT_NE(uuid1, uuid2);
}
+301
View File
@@ -0,0 +1,301 @@
#include "network/api_client.h"
#include "network/json.h"
#include "network/proclamation_sync.h"
#include "db/database_manager.h"
#include <gtest/gtest.h>
#include <cstdlib>
#include <string>
#include <vector>
using namespace radiantwave::api;
namespace {
// Reads an env var, returning "" when unset.
std::string env(const char* name) {
const char* value = std::getenv(name);
return value ? std::string(value) : std::string();
}
// Returns the optional live API key, or "" when none was provided.
std::string liveApiKey() {
return env("RW_LIVE_API_KEY");
}
} // namespace
// ─── Offline: parse functions ─────────────────────────────────────────
TEST(ApiClientTest, ParseFrequenciesJson) {
const std::string body =
R"([{"id":1,"name":"Crown","value":936},
{"id":5,"name":"Solar Plexus","value":528},
{"id":7,"name":"Root","value":396}])";
std::vector<Frequency> out;
EXPECT_TRUE(parseFrequenciesJson(body, out));
ASSERT_EQ(out.size(), 3u);
EXPECT_EQ(out[0].id, 1);
EXPECT_EQ(out[0].name, "Crown");
EXPECT_EQ(out[0].value, 936);
EXPECT_EQ(out[1].id, 5);
EXPECT_EQ(out[1].name, "Solar Plexus");
EXPECT_EQ(out[1].value, 528);
EXPECT_EQ(out[2].id, 7);
EXPECT_EQ(out[2].name, "Root");
EXPECT_EQ(out[2].value, 396);
}
TEST(ApiClientTest, ParseFrequenciesJsonEmpty) {
std::vector<Frequency> out;
EXPECT_TRUE(parseFrequenciesJson("[]", out));
EXPECT_TRUE(out.empty());
}
TEST(ApiClientTest, ParseFrequenciesJsonRejectsMalformed) {
std::vector<Frequency> out;
EXPECT_FALSE(parseFrequenciesJson("not json", out));
EXPECT_FALSE(parseFrequenciesJson(R"({"id":1})", out)); // object, not array
EXPECT_FALSE(parseFrequenciesJson(R"([{"id":1}", ])", out));
}
TEST(ApiClientTest, ParseFrequenciesJsonSkipsMissingId) {
std::vector<Frequency> out;
EXPECT_TRUE(parseFrequenciesJson(
R"([{"name":"NoId"},{"id":3,"name":"Throat","value":741}])", out));
ASSERT_EQ(out.size(), 1u);
EXPECT_EQ(out[0].id, 3);
}
TEST(ApiClientTest, ParseLicenseJsonLegacyStringLevel) {
const std::string body =
R"({"key":"ABCD-EFGH-IJKL-MNOP","email_address":"user@example.com","subscription_level":"Basic"})";
LicenseInfo info;
EXPECT_TRUE(parseLicenseJson(body, info));
EXPECT_EQ(info.key, "ABCD-EFGH-IJKL-MNOP");
EXPECT_EQ(info.email_address, "user@example.com");
EXPECT_EQ(info.subscription_level.name, "Basic");
EXPECT_EQ(info.subscription_level.id, 0);
}
TEST(ApiClientTest, ParseLicenseJsonObjectLevel) {
const std::string body =
R"({"key":"ABCD-EFGH-IJKL-MNOP","email_address":"user@example.com",)"
R"("subscription_level":{"id":2,"name":"Advanced","description":"General + Specific"}})";
LicenseInfo info;
EXPECT_TRUE(parseLicenseJson(body, info));
EXPECT_EQ(info.key, "ABCD-EFGH-IJKL-MNOP");
EXPECT_EQ(info.email_address, "user@example.com");
EXPECT_EQ(info.subscription_level.id, 2);
EXPECT_EQ(info.subscription_level.name, "Advanced");
EXPECT_EQ(info.subscription_level.description, "General + Specific");
}
TEST(ApiClientTest, ParseLicenseJsonFlatLevel) {
const std::string body =
R"({"key":"ABCD-EFGH-IJKL-MNOP","email_address":"user@example.com",)"
R"("subscription_level":"Elite","subscription_level_id":3,)"
R"("subscription_level_description":"General + Specific + Precise"})";
LicenseInfo info;
EXPECT_TRUE(parseLicenseJson(body, info));
EXPECT_EQ(info.subscription_level.id, 3);
EXPECT_EQ(info.subscription_level.name, "Elite");
EXPECT_EQ(info.subscription_level.description, "General + Specific + Precise");
}
TEST(ApiClientTest, ParseLicenseJsonRejectsMalformed) {
LicenseInfo info;
EXPECT_FALSE(parseLicenseJson("garbage", info));
EXPECT_FALSE(parseLicenseJson(R"({"key":""})", info));
}
TEST(ApiClientTest, ParseCreateAccountJson) {
const std::string body =
R"({"success":true,"message":"Account and license key created","user_id":"abc-123"})";
AccountBody account;
EXPECT_TRUE(parseCreateAccountJson(body, account));
EXPECT_TRUE(account.success);
EXPECT_EQ(account.message, "Account and license key created");
EXPECT_EQ(account.user_id, "abc-123");
}
TEST(ApiClientTest, ParseCreateAccountJsonRejectsMalformed) {
AccountBody account;
EXPECT_FALSE(parseCreateAccountJson("nope", account));
EXPECT_FALSE(parseCreateAccountJson("[]", account));
}
// ─── Offline: error message extraction ────────────────────────────────
TEST(ApiClientTest, ExtractErrorMessage) {
EXPECT_EQ(radiantwave::json::extractErrorMessage(R"({"message":"license key not found"})"),
"license key not found");
EXPECT_EQ(radiantwave::json::extractErrorMessage(R"({"error":"API key required"})"),
"API key required");
EXPECT_EQ(radiantwave::json::extractErrorMessage(R"({"code":400})"), "");
EXPECT_EQ(radiantwave::json::extractErrorMessage("not json"), "");
}
// ─── Offline: real-world payload shapes ────────────────────────────────
TEST(ApiClientTest, ParseProclamationsHandlesSubscriptionLevelIdNull) {
const std::string body =
R"([{"id":"a","name":"Peace","content":"<p>I am peace.</p>","categoryId":5,
"solfeggioFrequencyId":5,"subscriptionLevelId":null},
{"id":"b","name":"Foot","content":"<p>Heal.</p>","categoryId":32,
"solfeggioFrequencyId":7,"subscriptionLevelId":1}])";
std::vector<radiantwave::db::ProclamationRecord> out;
EXPECT_TRUE(radiantwave::sync::parseProclamationsJson(body, out));
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0].title, "Peace");
EXPECT_EQ(out[0].category_id, 5);
EXPECT_EQ(out[0].solfeggio_frequency_id, 5);
EXPECT_EQ(out[1].category_id, 32);
}
TEST(ApiClientTest, ParseCategoriesHandlesNullParentId) {
const std::string body =
R"([{"id":4,"name":"Spiritual","sortOrder":0,"parentId":null},
{"id":5,"name":"General","sortOrder":0,"parentId":4}])";
std::vector<radiantwave::db::ProclamationCategoryRecord> out;
EXPECT_TRUE(radiantwave::sync::parseCategoriesJson(body, out));
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0].parent_id, 0); // null parentId maps to 0 (root)
EXPECT_EQ(out[1].parent_id, 4);
}
// ─── Live: GET /frequencies (harmless, always runs) ───────────────────
TEST(ApiClientLiveTest, FetchFrequenciesLive) {
std::vector<Frequency> out;
EXPECT_TRUE(fetchFrequencies(out, liveApiKey()));
ASSERT_GE(out.size(), 7u);
bool foundSolarPlexus = false;
for (const Frequency& f : out) {
if (f.id == 5) {
foundSolarPlexus = true;
EXPECT_EQ(f.name, "Solar Plexus");
EXPECT_EQ(f.value, 528);
}
}
EXPECT_TRUE(foundSolarPlexus);
}
// ─── Live: proclamation + category parsers against the real API ────────
TEST(ApiClientLiveTest, ProclamationsAndCategoriesLiveSmoke) {
HttpResponse procsResponse = httpGet(PATH_PROCLAMATIONS, liveApiKey());
ASSERT_TRUE(procsResponse.transportOk);
ASSERT_EQ(procsResponse.status, 200);
std::vector<radiantwave::db::ProclamationRecord> proclamations;
EXPECT_TRUE(radiantwave::sync::parseProclamationsJson(procsResponse.body, proclamations));
EXPECT_GE(proclamations.size(), 50u);
if (!proclamations.empty()) {
// HTML tags must be stripped from the content.
EXPECT_EQ(proclamations[0].content.find('<'), std::string::npos);
EXPECT_FALSE(proclamations[0].id.empty());
}
HttpResponse catsResponse = httpGet(PATH_CATEGORIES, liveApiKey());
ASSERT_TRUE(catsResponse.transportOk);
ASSERT_EQ(catsResponse.status, 200);
std::vector<radiantwave::db::ProclamationCategoryRecord> categories;
EXPECT_TRUE(radiantwave::sync::parseCategoriesJson(catsResponse.body, categories));
EXPECT_GE(categories.size(), 20u);
}
// ─── Live: POST /license error paths (no credentials required) ─────────
TEST(ApiClientLiveTest, ValidateLicenseMissingKeyLive) {
LicenseResult result = validateLicense("", "test@example.com", liveApiKey());
ASSERT_FALSE(result.ok());
EXPECT_EQ(result.status, 400);
EXPECT_EQ(result.error_message, "license_key is required");
}
TEST(ApiClientLiveTest, ValidateLicenseMissingEmailLive) {
LicenseResult result = validateLicense("AAAA-AAAA-AAAA-AAAA", "", liveApiKey());
ASSERT_FALSE(result.ok());
EXPECT_EQ(result.status, 400);
EXPECT_EQ(result.error_message, "email_address is required");
}
TEST(ApiClientLiveTest, ValidateLicenseUnknownKeyLive) {
LicenseResult result =
validateLicense("AAAA-AAAA-AAAA-AAAA", "nobody@example.com", liveApiKey());
ASSERT_FALSE(result.ok());
EXPECT_EQ(result.status, 400);
EXPECT_EQ(result.error_message, "license key not found");
}
// ─── Live: POST /license success path (requires env credentials) ───────
TEST(ApiClientLiveTest, ValidateLicenseSuccessLive) {
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";
}
LicenseResult result = validateLicense(key, email, liveApiKey());
ASSERT_TRUE(result.ok()) << "status=" << result.status
<< " error=" << result.error_message;
EXPECT_EQ(result.info.key, key);
EXPECT_EQ(result.info.email_address, email);
EXPECT_FALSE(result.info.subscription_level.name.empty());
}
// ─── Live: POST /automations/create/account error paths (harmless) ─────
TEST(ApiClientLiveTest, CreateAccountMissingEmailLive) {
AccountResult result = createAccount("", "Basic", "", liveApiKey());
ASSERT_FALSE(result.ok());
EXPECT_EQ(result.status, 400);
EXPECT_EQ(result.error_message, "email is required");
}
TEST(ApiClientLiveTest, CreateAccountMissingLevelLive) {
AccountResult result = createAccount("test@example.com", "", "", liveApiKey());
ASSERT_FALSE(result.ok());
EXPECT_EQ(result.status, 400);
EXPECT_EQ(result.error_message, "subscription_level is required");
}
TEST(ApiClientLiveTest, CreateAccountInvalidLevelLive) {
AccountResult result =
createAccount("test@example.com", "NoSuchLevel123", "", liveApiKey());
ASSERT_FALSE(result.ok());
EXPECT_EQ(result.status, 400);
EXPECT_NE(result.error_message.find("not found"), std::string::npos);
}
// ─── Live: POST /automations/create/account success (creates a real
// ─── account + emails the user; requires RW_LIVE_ACCOUNT_EMAIL) ────────
TEST(ApiClientLiveTest, CreateAccountSuccessLive) {
const std::string email = env("RW_LIVE_ACCOUNT_EMAIL");
if (email.empty()) {
GTEST_SKIP() << "RW_LIVE_ACCOUNT_EMAIL not set";
}
AccountResult result = createAccount(email, "Basic", "RadiantWave Test", liveApiKey());
ASSERT_TRUE(result.ok()) << "status=" << result.status
<< " error=" << result.error_message;
EXPECT_TRUE(result.body.success);
EXPECT_EQ(result.body.message, "Account and license key created");
EXPECT_FALSE(result.body.user_id.empty());
}
+19
View File
@@ -0,0 +1,19 @@
#include "test_harness.h"
#include <filesystem>
TEST(fonts_exist)
{
ASSERT_TRUE(std::filesystem::exists("assets/fonts/Roboto-Regular.ttf"));
}
TEST(rd_title_image_exists)
{
ASSERT_TRUE(std::filesystem::exists("assets/images/RDW-Title-White-medium.png"));
}
TEST(rwt_circular_image_exists)
{
ASSERT_TRUE(std::filesystem::exists("assets/images/RWT-Circular-Color-ForOnDark-NoText.png"));
}
RUN_ALL_TESTS()
+38
View File
@@ -0,0 +1,38 @@
#include "audio/audio_manager.h"
#include <gtest/gtest.h>
using namespace radiantwave::audio;
TEST(AudioTest, FriendlyDeviceNameMapping) {
EXPECT_EQ(mapToFriendlyDeviceName("alsa_output.pci-0000_00_1f.3.analog-stereo"), "Built-In Speakers");
EXPECT_EQ(mapToFriendlyDeviceName("AMD Radeon HD Audio Controller HDMI"), "TV / HDMI Monitor");
EXPECT_EQ(mapToFriendlyDeviceName("USB Audio Headset Dongle"), "USB Audio Device");
EXPECT_EQ(mapToFriendlyDeviceName("bluez_sink.00_11_22_33_44_55"), "Bluetooth Audio");
EXPECT_EQ(mapToFriendlyDeviceName(""), "Default Audio Output");
}
TEST(AudioTest, AudioManagerVolumeClamping) {
radiantwave::config::ConfigManager config;
std::string temp_path = (std::filesystem::temp_directory_path() / "test_rw_audio_cfg.yaml").string();
if (std::filesystem::exists(temp_path)) {
std::filesystem::remove(temp_path);
}
ASSERT_TRUE(config.initialize(temp_path));
AudioManager am;
// Test initialization without audio device (graceful fallback)
am.initialize(config);
am.setVolume(150);
EXPECT_EQ(am.getVolume(), 100);
am.setVolume(-20);
EXPECT_EQ(am.getVolume(), 0);
am.setVolume(75);
EXPECT_EQ(am.getVolume(), 75);
am.shutdown();
std::filesystem::remove(temp_path);
}
+154
View File
@@ -0,0 +1,154 @@
#include "audio/audio_downloader.h"
#include <gtest/gtest.h>
#include <unistd.h>
#include <fstream>
#include <string>
#include <vector>
namespace radiantwave::audio {
namespace {
std::string tempPath(const std::string& suffix) {
return "/tmp/radiantwave_ad_test_" + std::to_string(static_cast<long>(getpid())) + suffix;
}
TEST(AudioDownloaderManifest, ParsesCurrentFormatWithDisplayName) {
const std::string text =
"# checksums generated by md5sum\n"
"174Hz.wav '174 Hz Solfeggio' 174 5d41402abc4b2a76b9719d911017c592\n"
"PinealGland.wav 'Pineal Gland Activator' 0 e99a18c428cb38d5f260853678922e03\n";
std::vector<AudioManifestEntry> manifest;
ASSERT_TRUE(parseManifest(text, manifest));
ASSERT_EQ(manifest.size(), 2u);
EXPECT_EQ(manifest[0].filename, "174Hz.wav");
EXPECT_EQ(manifest[0].display_name, "174 Hz Solfeggio");
EXPECT_EQ(manifest[0].solfeggio_frequency_hz, 174);
EXPECT_EQ(manifest[0].md5, "5d41402abc4b2a76b9719d911017c592");
EXPECT_EQ(manifest[1].filename, "PinealGland.wav");
EXPECT_EQ(manifest[1].display_name, "Pineal Gland Activator");
EXPECT_EQ(manifest[1].solfeggio_frequency_hz, 0);
EXPECT_EQ(manifest[1].md5, "e99a18c428cb38d5f260853678922e03");
}
TEST(AudioDownloaderManifest, ParsesZeroHzAsNull) {
const std::string text = "OriginalAudio.wav 'Original Audio' 0 5d41402abc4b2a76b9719d911017c592\n";
std::vector<AudioManifestEntry> manifest;
ASSERT_TRUE(parseManifest(text, manifest));
ASSERT_EQ(manifest.size(), 1u);
EXPECT_EQ(manifest[0].solfeggio_frequency_hz, 0);
}
TEST(AudioDownloaderManifest, FallsBackToFilenameForUnrecognizedHzToken) {
// A transitional manifest may still carry a frequency id (or any
// unrecognized number) in token 2; the filename Hz must win.
const std::string text =
"396Hz.wav '396 Hz' 2 5d41402abc4b2a76b9719d911017c592\n"
"1111Hz.wav '1111 Hz' 10 e99a18c428cb38d5f260853678922e03\n";
std::vector<AudioManifestEntry> manifest;
ASSERT_TRUE(parseManifest(text, manifest));
ASSERT_EQ(manifest.size(), 2u);
EXPECT_EQ(manifest[0].solfeggio_frequency_hz, 396);
EXPECT_EQ(manifest[1].solfeggio_frequency_hz, 1111);
}
TEST(AudioDownloaderManifest, ParsesLegacyMd5sumFormat) {
const std::string text =
"5d41402abc4b2a76b9719d911017c592 hello.txt\n"
"e99a18c428cb38d5f260853678922e03 *396Hz.wav\n"
"a9993e364706816aba3e25717850c26c ./nested.wav\n";
std::vector<AudioManifestEntry> manifest;
ASSERT_TRUE(parseManifest(text, manifest));
ASSERT_EQ(manifest.size(), 3u);
EXPECT_EQ(manifest[0].filename, "hello.txt");
EXPECT_EQ(manifest[0].display_name, "hello");
EXPECT_EQ(manifest[0].solfeggio_frequency_hz, 0);
EXPECT_EQ(manifest[0].md5, "5d41402abc4b2a76b9719d911017c592");
EXPECT_EQ(manifest[1].filename, "396Hz.wav");
EXPECT_EQ(manifest[1].solfeggio_frequency_hz, 396);
EXPECT_EQ(manifest[1].md5, "e99a18c428cb38d5f260853678922e03");
EXPECT_EQ(manifest[2].filename, "nested.wav");
EXPECT_EQ(manifest[2].solfeggio_frequency_hz, 0);
EXPECT_EQ(manifest[2].md5, "a9993e364706816aba3e25717850c26c");
}
TEST(AudioDownloaderManifest, SkipsMalformedLines) {
const std::string text =
"not-a-valid-line\n"
"5d41402abc4b2a76b9719d911017c592 ok.txt\n"
"1111Hz.wav 'Display Name With Spaces' 10 5d41402abc4b2a76b9719d911017c592\n";
std::vector<AudioManifestEntry> manifest;
ASSERT_TRUE(parseManifest(text, manifest));
ASSERT_EQ(manifest.size(), 2u);
EXPECT_EQ(manifest[0].filename, "ok.txt");
EXPECT_EQ(manifest[0].solfeggio_frequency_hz, 0);
EXPECT_EQ(manifest[1].filename, "1111Hz.wav");
EXPECT_EQ(manifest[1].display_name, "Display Name With Spaces");
EXPECT_EQ(manifest[1].solfeggio_frequency_hz, 1111);
}
TEST(AudioDownloaderManifest, EmptyOrCommentedTextFails) {
std::vector<AudioManifestEntry> manifest;
EXPECT_FALSE(parseManifest("", manifest));
EXPECT_FALSE(parseManifest("# only a comment\n", manifest));
}
TEST(AudioDownloaderFrequency, MapsKnownHzToIds) {
EXPECT_EQ(frequencyIdForHz(174), 1);
EXPECT_EQ(frequencyIdForHz(285), 1);
EXPECT_EQ(frequencyIdForHz(396), 2);
EXPECT_EQ(frequencyIdForHz(417), 3);
EXPECT_EQ(frequencyIdForHz(432), 4);
EXPECT_EQ(frequencyIdForHz(528), 5);
EXPECT_EQ(frequencyIdForHz(639), 6);
EXPECT_EQ(frequencyIdForHz(741), 7);
EXPECT_EQ(frequencyIdForHz(852), 8);
EXPECT_EQ(frequencyIdForHz(963), 9);
EXPECT_EQ(frequencyIdForHz(1111), 0);
EXPECT_EQ(frequencyIdForHz(0), 0);
EXPECT_EQ(frequencyIdForHz(-5), 0);
}
TEST(AudioDownloaderFrequency, ParsesHzFromFilenames) {
EXPECT_EQ(hzFromFilename("396Hz.wav"), 396);
EXPECT_EQ(hzFromFilename("14Hz_Binaural.wav"), 14);
EXPECT_EQ(hzFromFilename("1111Hz.wav"), 1111);
EXPECT_EQ(hzFromFilename("396hz.wav"), 396);
EXPECT_EQ(hzFromFilename("AlphaWaves.wav"), 0);
EXPECT_EQ(hzFromFilename("OriginalAudio.wav"), 0);
EXPECT_EQ(hzFromFilename(""), 0);
EXPECT_EQ(hzFromFilename("1x396Hz.wav"), 0);
}
TEST(AudioDownloaderMd5, ComputesKnownDigest) {
std::string path = tempPath("_hello.txt");
{
std::ofstream out(path, std::ios::binary);
out << "hello";
}
EXPECT_EQ(computeFileMd5(path), "5d41402abc4b2a76b9719d911017c592");
std::remove(path.c_str());
}
TEST(AudioDownloaderMd5, EmptyFileDigest) {
std::string path = tempPath("_empty.txt");
{
std::ofstream out(path, std::ios::binary);
}
EXPECT_EQ(computeFileMd5(path), "d41d8cd98f00b204e9800998ecf8427e");
std::remove(path.c_str());
}
TEST(AudioDownloaderMd5, MissingFileReturnsEmpty) {
EXPECT_EQ(computeFileMd5(tempPath("_missing.bin")), "");
}
} // namespace
} // namespace radiantwave::audio
+183
View File
@@ -0,0 +1,183 @@
#include "db/database_manager.h"
#include "network/api_client.h"
#include "network/proclamation_sync.h"
#include <gtest/gtest.h>
#include <cstdlib>
#include <filesystem>
#include <string>
#include <vector>
using namespace radiantwave::api;
namespace {
// Reads an env var, returning "" when unset.
std::string env(const char* name) {
const char* value = std::getenv(name);
return value ? std::string(value) : std::string();
}
} // namespace
// ─── Offline: GET /color-palettes parse ────────────────────────────────
TEST(ColorPalettesTest, ParseColorPalettesJson) {
const std::string body =
R"([
{"solfeggioFrequencyId":1,
"colors":["#8B6914","#B8860B","#DAA520","#E8B64C","#FFD700","#FFE9A8","#FFF4CC"]},
{"solfeggioFrequencyId":5,
"colors":["#32CD32","#3CB371","#90EE90","#98FB98","#ADFFA9","#C8FFD2","#F0FFF0"]}
])";
std::vector<ColorPalette> out;
EXPECT_TRUE(parseColorPalettesJson(body, out));
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0].solfeggio_frequency_id, 1);
ASSERT_EQ(out[0].colors.size(), 7u);
EXPECT_EQ(out[0].colors[0].r, 0x8B);
EXPECT_EQ(out[0].colors[0].g, 0x69);
EXPECT_EQ(out[0].colors[0].b, 0x14);
EXPECT_EQ(out[0].colors[6].b, 0xCC);
EXPECT_EQ(out[1].solfeggio_frequency_id, 5);
ASSERT_EQ(out[1].colors.size(), 7u);
EXPECT_EQ(out[1].colors[0].r, 0x32);
EXPECT_EQ(out[1].colors[0].g, 0xCD);
EXPECT_EQ(out[1].colors[0].b, 0x32);
}
TEST(ColorPalettesTest, ParseColorPalettesJsonAcceptsHexWithoutHash) {
const std::string body = R"([{"solfeggioFrequencyId":1,"colors":["32CD32"]}])";
std::vector<ColorPalette> out;
EXPECT_TRUE(parseColorPalettesJson(body, out));
ASSERT_EQ(out.size(), 1u);
ASSERT_EQ(out[0].colors.size(), 1u);
EXPECT_EQ(out[0].colors[0].r, 0x32);
EXPECT_EQ(out[0].colors[0].g, 0xCD);
EXPECT_EQ(out[0].colors[0].b, 0x32);
}
TEST(ColorPalettesTest, ParseColorPalettesJsonSkipsMissingFrequencyId) {
const std::string body =
R"([{"colors":["#32CD32"]},{"solfeggioFrequencyId":5,"colors":["#00FF7F"]}])";
std::vector<ColorPalette> out;
EXPECT_TRUE(parseColorPalettesJson(body, out));
ASSERT_EQ(out.size(), 1u);
EXPECT_EQ(out[0].solfeggio_frequency_id, 5);
}
TEST(ColorPalettesTest, ParseColorPalettesJsonEmptyAndMalformed) {
std::vector<ColorPalette> out;
EXPECT_TRUE(parseColorPalettesJson("[]", out));
EXPECT_TRUE(out.empty());
out.clear();
EXPECT_FALSE(parseColorPalettesJson("not json", out));
EXPECT_FALSE(parseColorPalettesJson(R"({"solfeggioFrequencyId":1})", out));
}
// ─── Offline: palettes embedded in /proclamations payload ──────────────
TEST(ColorPalettesTest, ParseProclamationPalettesJson) {
const std::string body =
R"PALETTE([
{"id":"p1","name":"One","solfeggioFrequencyId":1,
"colorPalette":["#8B6914","#B8860B"]},
{"id":"p2","name":"Two","solfeggioFrequencyId":5,
"colorPalette":["#32CD32","#3CB371","#90EE90"]},
{"id":"p3","name":"Also freq 5 (ignored, first wins)",
"solfeggioFrequencyId":5,
"colorPalette":["#FF0000"]}
])PALETTE";
std::vector<radiantwave::db::PaletteRecord> out;
EXPECT_TRUE(radiantwave::sync::parseProclamationPalettesJson(body, out));
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0].solfeggio_frequency_id, 1);
ASSERT_EQ(out[0].colors.size(), 2u);
EXPECT_EQ(out[0].colors[0].r, 0x8B);
EXPECT_EQ(out[1].solfeggio_frequency_id, 5);
ASSERT_EQ(out[1].colors.size(), 3u);
EXPECT_EQ(out[1].colors[0].r, 0x32); // not #FF0000: first proclamation wins
}
TEST(ColorPalettesTest, ParseProclamationPalettesJsonSkipsMissingPalette) {
const std::string body =
R"([{"id":"p1","name":"No palette","solfeggioFrequencyId":1}])";
std::vector<radiantwave::db::PaletteRecord> out;
EXPECT_TRUE(radiantwave::sync::parseProclamationPalettesJson(body, out));
EXPECT_TRUE(out.empty());
}
// ─── Offline: DB round-trip via replaceColorPalettes ───────────────────
TEST(ColorPalettesTest, ReplaceColorPalettesRoundTrip) {
const std::string temp_path =
(std::filesystem::temp_directory_path() / "test_rw_db_palettesync.db").string();
if (std::filesystem::exists(temp_path)) {
std::filesystem::remove(temp_path);
}
radiantwave::db::DatabaseManager db;
ASSERT_TRUE(db.initialize(temp_path));
radiantwave::db::PaletteRecord rec;
rec.solfeggio_frequency_id = 1;
rec.colors = {{0x8B, 0x69, 0x14, 255}, {0xFF, 0xD7, 0x00, 255}};
ASSERT_TRUE(db.replaceColorPalettes({rec}));
auto palette = db.getPaletteForFrequencyId(1);
ASSERT_EQ(palette.size(), 2u);
EXPECT_EQ(palette[0].r, 0x8B);
EXPECT_EQ(palette[0].g, 0x69);
EXPECT_EQ(palette[0].b, 0x14);
EXPECT_EQ(palette[1].r, 0xFF);
EXPECT_EQ(palette[1].g, 0xD7);
EXPECT_EQ(palette[1].b, 0x00);
db.close();
std::filesystem::remove(temp_path);
}
TEST(ColorPalettesTest, ReplaceColorPalettesEmptyClearsAndFallsBackTo528) {
const std::string temp_path =
(std::filesystem::temp_directory_path() / "test_rw_db_paletteclear.db").string();
if (std::filesystem::exists(temp_path)) {
std::filesystem::remove(temp_path);
}
radiantwave::db::DatabaseManager db;
ASSERT_TRUE(db.initialize(temp_path));
ASSERT_TRUE(db.replaceColorPalettes({}));
// id 5 was cleared, so both the direct lookup and the 528 Hz fallback are empty.
EXPECT_TRUE(db.getPaletteForFrequencyId(5).empty());
db.close();
std::filesystem::remove(temp_path);
}
// ─── Live: GET /color-palettes (route not live yet; opt-in) ────────────
TEST(ColorPalettesLiveTest, FetchColorPalettesLive) {
if (env("RW_LIVE_COLOR_PALETTES").empty()) {
GTEST_SKIP() << "RW_LIVE_COLOR_PALETTES not set (route not live yet)";
}
std::vector<ColorPalette> palettes;
ASSERT_TRUE(fetchColorPalettes(palettes, env("RW_LIVE_API_KEY")));
EXPECT_FALSE(palettes.empty());
for (const auto& p : palettes) {
EXPECT_GE(p.colors.size(), 1u);
}
}
+138
View File
@@ -0,0 +1,138 @@
#include "config/config_manager.h"
#include "config/yaml_parser.h"
#include <gtest/gtest.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <thread>
using namespace radiantwave::config;
TEST(YamlParserTest, ParseBasicYaml) {
std::string yaml = R"(
license_key: "KEY-12345"
email_address: "test@example.com"
provisioning_status: "READY"
audio:
volume: 95
output_device: "hdmi"
proclamation:
scroll_speed: 2.5
font_size: 40
)";
YamlNode root = parseYaml(yaml);
EXPECT_TRUE(root.hasChild("license_key"));
EXPECT_EQ(root.getChild("license_key").asString(), "KEY-12345");
EXPECT_EQ(root.getChild("email_address").asString(), "test@example.com");
EXPECT_TRUE(root.hasChild("audio"));
EXPECT_EQ(root.getChild("audio").getChild("volume").asInt(), 95);
EXPECT_EQ(root.getChild("audio").getChild("output_device").asString(), "hdmi");
EXPECT_TRUE(root.hasChild("proclamation"));
EXPECT_FLOAT_EQ(root.getChild("proclamation").getChild("scroll_speed").asFloat(), 2.5f);
EXPECT_EQ(root.getChild("proclamation").getChild("font_size").asInt(), 40);
}
TEST(YamlParserTest, ConfigRoundTrip) {
AppConfig original;
original.license_key = "RW-TEST-99";
original.email_address = "admin@radiantwave.com";
original.audio.volume = 60;
original.audio.dynamic_audio = false;
original.proclamation.scroll_speed = 1.8f;
std::string serialized = serializeConfigToYaml(original);
AppConfig parsed = parseConfigFromYaml(serialized);
EXPECT_EQ(parsed.license_key, original.license_key);
EXPECT_EQ(parsed.email_address, original.email_address);
EXPECT_EQ(parsed.audio.volume, original.audio.volume);
EXPECT_EQ(parsed.audio.dynamic_audio, original.audio.dynamic_audio);
EXPECT_FLOAT_EQ(parsed.proclamation.scroll_speed, original.proclamation.scroll_speed);
}
TEST(ConfigManagerTest, DefaultGeneration) {
std::string temp_path = (std::filesystem::temp_directory_path() / "test_rw_config1.yaml").string();
if (std::filesystem::exists(temp_path)) {
std::filesystem::remove(temp_path);
}
ConfigManager manager;
EXPECT_TRUE(manager.initialize(temp_path));
AppConfig cfg = manager.get();
EXPECT_EQ(cfg.audio.volume, 80);
EXPECT_EQ(cfg.audio.dynamic_audio, true);
EXPECT_EQ(cfg.proclamation.font_size, 32);
EXPECT_TRUE(std::filesystem::exists(temp_path));
std::filesystem::remove(temp_path);
}
TEST(ConfigManagerTest, MergePreservesExistingValues) {
std::string temp_path = (std::filesystem::temp_directory_path() / "test_rw_config2.yaml").string();
// Write partial config
{
std::ofstream file(temp_path);
file << "license_key: \"MY-LICENSE\"\n";
file << "audio:\n";
file << " volume: 42\n";
}
ConfigManager manager;
EXPECT_TRUE(manager.initialize(temp_path));
AppConfig cfg = manager.get();
// Preserved custom values
EXPECT_EQ(cfg.license_key, "MY-LICENSE");
EXPECT_EQ(cfg.audio.volume, 42);
// Default values populated for missing fields
EXPECT_EQ(cfg.proclamation.font_size, 32);
std::filesystem::remove(temp_path);
}
TEST(ConfigManagerTest, HotReloadWatcher) {
std::string temp_path = (std::filesystem::temp_directory_path() / "test_rw_config3.yaml").string();
if (std::filesystem::exists(temp_path)) {
std::filesystem::remove(temp_path);
}
ConfigManager manager;
EXPECT_TRUE(manager.initialize(temp_path));
std::atomic<bool> reloaded{false};
int new_font_size = 0;
manager.onChanged([&](const AppConfig& new_cfg) {
new_font_size = new_cfg.proclamation.font_size;
reloaded = true;
});
// Pause briefly then write modified config to disk
std::this_thread::sleep_for(std::chrono::milliseconds(600));
{
std::ofstream file(temp_path, std::ios::out | std::ios::trunc);
file << "proclamation:\n";
file << " font_size: 48\n";
}
// Wait for file watcher thread to pick up change
for (int i = 0; i < 20; ++i) {
if (reloaded) break;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
EXPECT_TRUE(reloaded);
EXPECT_EQ(new_font_size, 48);
manager.shutdown();
std::filesystem::remove(temp_path);
}
+61
View File
@@ -0,0 +1,61 @@
#include "date_format.h"
#include <gtest/gtest.h>
#include <cstdlib>
#include <ctime>
using namespace radiantwave::datetime;
class DateFormatTest : public ::testing::Test {
protected:
static void SetUpTestSuite() {
setenv("TZ", "UTC", 1);
tzset();
}
};
TEST_F(DateFormatTest, DefaultFormatConstant) {
EXPECT_STREQ(DEFAULT_DATE_FORMAT, "%m/%d/%Y");
}
TEST_F(DateFormatTest, FormatsKnownTimestamp) {
std::tm tm = {};
tm.tm_year = 2026 - 1900;
tm.tm_mon = 8 - 1;
tm.tm_mday = 4;
tm.tm_hour = 14;
tm.tm_min = 23;
tm.tm_sec = 45;
tm.tm_isdst = -1;
time_t t = mktime(&tm);
ASSERT_NE(t, static_cast<time_t>(-1));
EXPECT_EQ(formatDate(t, "%m/%d/%Y"), "08/04/2026");
EXPECT_EQ(formatDate(t, "%d/%m/%Y"), "04/08/2026");
EXPECT_EQ(formatDate(t, "%Y-%m-%d"), "2026-08-04");
EXPECT_EQ(formatDate(t, "%m.%d.%Y"), "08.04.2026");
EXPECT_EQ(formatDate(t, "%d.%m.%Y"), "04.08.2026");
EXPECT_EQ(formatDate(t, "%B %d, %Y"), "August 04, 2026");
EXPECT_EQ(formatDate(t, "%d %B %Y"), "04 August 2026");
}
TEST_F(DateFormatTest, FormatsTime) {
std::tm tm = {};
tm.tm_year = 2026 - 1900;
tm.tm_mon = 8 - 1;
tm.tm_mday = 4;
tm.tm_hour = 14;
tm.tm_min = 23;
tm.tm_sec = 45;
tm.tm_isdst = -1;
time_t t = mktime(&tm);
ASSERT_NE(t, static_cast<time_t>(-1));
EXPECT_EQ(formatTime(t), "14:23:45");
}
TEST_F(DateFormatTest, OptionsIncludeDefault) {
auto options = getDateFormatOptions();
ASSERT_FALSE(options.empty());
EXPECT_EQ(options[0].pattern, std::string(DEFAULT_DATE_FORMAT));
EXPECT_EQ(options[0].name, "Month/Day/Year");
}
+322
View File
@@ -0,0 +1,322 @@
#include "db/database_manager.h"
#include <gtest/gtest.h>
#include <filesystem>
using namespace radiantwave::db;
TEST(DatabaseManagerTest, InitializeAndMigrate) {
std::string temp_path = (std::filesystem::temp_directory_path() / "test_rw_db1.db").string();
if (std::filesystem::exists(temp_path)) {
std::filesystem::remove(temp_path);
}
DatabaseManager db;
EXPECT_TRUE(db.initialize(temp_path));
EXPECT_TRUE(std::filesystem::exists(temp_path));
db.close();
std::filesystem::remove(temp_path);
}
TEST(DatabaseManagerTest, KeyValueStoreOperations) {
std::string temp_path = (std::filesystem::temp_directory_path() / "test_rw_db2.db").string();
if (std::filesystem::exists(temp_path)) {
std::filesystem::remove(temp_path);
}
DatabaseManager db;
ASSERT_TRUE(db.initialize(temp_path));
EXPECT_FALSE(db.hasKV("device_token"));
EXPECT_EQ(db.getKV("device_token", "default_token"), "default_token");
EXPECT_TRUE(db.setKV("device_token", "ABC-123-XYZ"));
EXPECT_TRUE(db.hasKV("device_token"));
EXPECT_EQ(db.getKV("device_token"), "ABC-123-XYZ");
// Upsert test
EXPECT_TRUE(db.setKV("device_token", "NEW-TOKEN-789"));
EXPECT_EQ(db.getKV("device_token"), "NEW-TOKEN-789");
// Delete test
EXPECT_TRUE(db.deleteKV("device_token"));
EXPECT_FALSE(db.hasKV("device_token"));
db.close();
std::filesystem::remove(temp_path);
}
TEST(DatabaseManagerTest, ProclamationsAndDownloads) {
std::string temp_path = (std::filesystem::temp_directory_path() / "test_rw_db3.db").string();
if (std::filesystem::exists(temp_path)) {
std::filesystem::remove(temp_path);
}
DatabaseManager db;
ASSERT_TRUE(db.initialize(temp_path));
ProclamationRecord proc;
proc.id = "p1";
proc.title = "Peace";
proc.content = "I am peace.";
proc.solfeggio_freq_hz = 528.0;
EXPECT_TRUE(db.saveProclamation(proc));
auto active = db.getActiveProclamations();
ASSERT_EQ(active.size(), 1u);
EXPECT_EQ(active[0].title, "Peace");
EXPECT_DOUBLE_EQ(active[0].solfeggio_freq_hz, 528.0);
AssetDownloadRecord dl;
dl.id = "a1";
dl.filename = "audio1.wav";
dl.url = "https://example.com/audio1.wav";
dl.status = "pending";
EXPECT_TRUE(db.updateAssetDownload(dl));
auto pending = db.getPendingAssetDownloads();
ASSERT_EQ(pending.size(), 1u);
EXPECT_EQ(pending[0].filename, "audio1.wav");
EXPECT_TRUE(db.logEvent("INFO", "Test", "All systems operational"));
db.close();
std::filesystem::remove(temp_path);
}
namespace {
// Helper: build a fresh in-memory-less temp DatabaseManager.
struct DbFixture {
std::string path;
DatabaseManager db;
DbFixture(const char* tag) {
path = (std::filesystem::temp_directory_path() / tag).string();
if (std::filesystem::exists(path)) {
std::filesystem::remove(path);
}
db.initialize(path);
}
~DbFixture() {
db.close();
std::filesystem::remove(path);
}
};
ProclamationRecord makeProc(const std::string& id, const std::string& title, int cat) {
ProclamationRecord p;
p.id = id;
p.title = title;
p.content = "content of " + title;
p.category_id = cat;
return p;
}
} // namespace
TEST(DatabaseManagerTest, SyncProclamationsPreservesSelectionAndDeletesRemoved) {
const std::string defId = "default-1";
DbFixture fx("test_rw_db_sync.db");
ProclamationRecord a = makeProc("a", "Alpha", 1);
ProclamationRecord b = makeProc("b", "Beta", 1);
// First sync inserts both. The incoming `selected` flag is never trusted.
ASSERT_TRUE(fx.db.syncProclamations({a, b}, defId));
// Simulate the user picking "a".
ASSERT_TRUE(fx.db.setProclamationSelected("a", true));
// Re-sync: "a" stays selected, "b" is removed because the API no longer
// returns it.
ASSERT_TRUE(fx.db.syncProclamations({a}, defId));
auto all = fx.db.getAllProclamations();
ASSERT_EQ(all.size(), 1u);
EXPECT_EQ(all[0].id, "a");
EXPECT_TRUE(all[0].selected);
}
TEST(DatabaseManagerTest, RequiredLevelIdRoundTripsThroughSync) {
const std::string defId = "default-1";
DbFixture fx("test_rw_db_level.db");
ProclamationRecord def = makeProc(defId, "Default", 1);
def.required_level_id = 1;
ProclamationRecord elite = makeProc("elite", "Elite Only", 2);
elite.required_level_id = 3;
ASSERT_TRUE(fx.db.syncProclamations({def, elite}, defId));
auto all = fx.db.getAllProclamations();
ASSERT_EQ(all.size(), 2u);
for (const auto& p : all) {
EXPECT_EQ(p.required_level_id, p.id == "elite" ? 3 : 1);
}
// A re-sync with the same records keeps the level (and selection).
ASSERT_TRUE(fx.db.syncProclamations({def, elite}, defId));
auto again = fx.db.getAllProclamations();
ASSERT_EQ(again.size(), 2u);
for (const auto& p : again) {
EXPECT_EQ(p.required_level_id, p.id == "elite" ? 3 : 1);
}
}
TEST(DatabaseManagerTest, SyncForcesDefaultProclamationSelected) {
const std::string defId = "default-1";
DbFixture fx("test_rw_db_def.db");
ProclamationRecord def = makeProc(defId, "Default", 3);
ProclamationRecord other = makeProc("x", "Other", 3);
def.selected = false;
ASSERT_TRUE(fx.db.syncProclamations({def, other}, defId));
ASSERT_TRUE(fx.db.ensureDefaultProclamationSelected(defId));
auto all = fx.db.getAllProclamations();
for (const auto& p : all) {
if (p.id == defId) {
EXPECT_TRUE(p.selected);
}
}
}
TEST(DatabaseManagerTest, SaveSelectedProclamationsClearsOthers) {
const std::string defId = "default-1";
DbFixture fx("test_rw_db_save.db");
ProclamationRecord def = makeProc(defId, "Default", 1);
ProclamationRecord a = makeProc("a", "A", 1);
ProclamationRecord b = makeProc("b", "B", 1);
ASSERT_TRUE(fx.db.syncProclamations({def, a, b}, defId));
ASSERT_TRUE(fx.db.setProclamationSelected("a", true));
// Deselect everything except a; the default must still end up selected.
ASSERT_TRUE(fx.db.saveSelectedProclamations({"a"}, defId));
auto all = fx.db.getAllProclamations();
for (const auto& p : all) {
bool expected = (p.id == "a" || p.id == defId);
EXPECT_EQ(p.selected, expected) << "proclamation " << p.id;
}
}
TEST(DatabaseManagerTest, ReplaceAndReadCategories) {
DbFixture fx("test_rw_db_cats.db");
std::vector<ProclamationCategoryRecord> cats;
cats.push_back({3, "Root", 0, 1});
cats.push_back({7, "Child", 3, 2});
ASSERT_TRUE(fx.db.replaceCategories(cats));
auto all = fx.db.getAllCategories();
ASSERT_EQ(all.size(), 2u);
EXPECT_EQ(all[0].id, 3);
EXPECT_EQ(all[0].parent_id, 0);
EXPECT_EQ(all[1].id, 7);
EXPECT_EQ(all[1].parent_id, 3);
// Replacing wipes previous categories.
ASSERT_TRUE(fx.db.replaceCategories({}));
EXPECT_EQ(fx.db.getAllCategories().size(), 0u);
}
TEST(DatabaseManagerTest, SyncAudioFilesUpsertsAndDeletesMissing) {
DbFixture fx("test_rw_db_audio.db");
std::vector<AudioFileRecord> first = {
{0, "174 Hz Solfeggio", "174Hz.wav", 1, 1},
{0, "Original Audio", "OriginalAudio.wav", 2, 0},
{0, "Alpha Waves", "AlphaWaves.wav", 3, 13},
};
ASSERT_TRUE(fx.db.syncAudioFiles(first));
auto rows = fx.db.getAudioFiles();
ASSERT_EQ(rows.size(), 3u);
EXPECT_EQ(rows[0].filename, "174Hz.wav");
EXPECT_EQ(rows[0].display_name, "174 Hz Solfeggio");
EXPECT_EQ(rows[0].display_order, 1);
EXPECT_EQ(rows[0].solfeggio_frequency_id, 1);
EXPECT_EQ(rows[1].filename, "OriginalAudio.wav");
EXPECT_EQ(rows[1].solfeggio_frequency_id, 0);
EXPECT_EQ(rows[2].filename, "AlphaWaves.wav");
EXPECT_EQ(rows[2].solfeggio_frequency_id, 13);
// Manifest update: renamed display name, reordered, one file removed.
std::vector<AudioFileRecord> second = {
{0, "174 Hz Solfeggio (Updated)", "174Hz.wav", 2, 1},
{0, "Alpha Waves", "AlphaWaves.wav", 1, 13},
};
ASSERT_TRUE(fx.db.syncAudioFiles(second));
rows = fx.db.getAudioFiles();
ASSERT_EQ(rows.size(), 2u);
EXPECT_EQ(rows[0].filename, "AlphaWaves.wav");
EXPECT_EQ(rows[0].display_order, 1);
EXPECT_EQ(rows[1].filename, "174Hz.wav");
EXPECT_EQ(rows[1].display_name, "174 Hz Solfeggio (Updated)");
EXPECT_EQ(rows[1].display_order, 2);
}
TEST(DatabaseManagerTest, SyncAudioFilesClearsEverythingWhenEmpty) {
DbFixture fx("test_rw_db_audio_empty.db");
ASSERT_TRUE(fx.db.syncAudioFiles({{0, "Original Audio", "OriginalAudio.wav", 1, 0}}));
ASSERT_EQ(fx.db.getAudioFiles().size(), 1u);
ASSERT_TRUE(fx.db.syncAudioFiles({}));
EXPECT_EQ(fx.db.getAudioFiles().size(), 0u);
}
TEST(DatabaseManagerTest, PalettesSeededAndRead) {
DbFixture fx("test_rw_db_palette.db");
auto palette = fx.db.getPaletteForFrequencyId(5); // 528 Hz
ASSERT_EQ(palette.size(), 7u);
EXPECT_EQ(palette[0].r, 0x32);
EXPECT_EQ(palette[0].g, 0xCD);
EXPECT_EQ(palette[0].b, 0x32);
EXPECT_EQ(palette[0].a, 255u);
EXPECT_EQ(palette[6].r, 0xF0);
EXPECT_EQ(palette[6].g, 0xFF);
EXPECT_EQ(palette[6].b, 0xF0);
auto red = fx.db.getPaletteForFrequencyId(2); // 396 Hz
ASSERT_EQ(red.size(), 7u);
EXPECT_EQ(red[0].r, 0x8B);
EXPECT_EQ(red[0].g, 0x00);
EXPECT_EQ(red[0].b, 0x00);
}
TEST(DatabaseManagerTest, UnknownFrequencyFallsBackTo528) {
DbFixture fx("test_rw_db_palette_fb.db");
auto palette = fx.db.getPaletteForFrequencyId(9999);
ASSERT_EQ(palette.size(), 7u);
EXPECT_EQ(palette[0].r, 0x32);
EXPECT_EQ(palette[0].g, 0xCD);
EXPECT_EQ(palette[0].b, 0x32);
}
TEST(DatabaseManagerTest, PaletteSeedIsIdempotent) {
DbFixture fx("test_rw_db_palette_seed.db");
// Re-initialize on the same file; seeding must not duplicate rows.
fx.db.close();
ASSERT_TRUE(fx.db.initialize(fx.path));
auto palette = fx.db.getPaletteForFrequencyId(9); // 963 Hz
ASSERT_EQ(palette.size(), 7u);
EXPECT_EQ(palette[0].r, 0x94);
EXPECT_EQ(palette[0].g, 0x00);
EXPECT_EQ(palette[0].b, 0xD3);
}
+125
View File
@@ -0,0 +1,125 @@
#include "fibonacci_line_pattern.h"
#include <gtest/gtest.h>
#include <string>
#include <vector>
namespace {
float widthOf(const std::string& word)
{
return static_cast<float>(word.size()) * 10.0f;
}
} // namespace
TEST(FibonacciLinePatternTest, WordCountFollowsFibonacciSequence)
{
radiantwave::FibonacciLinePattern pattern(13, 100000.0f, 10.0f, widthOf);
// Enough words that the first sixteen lines never reach the end of the list,
// so line counts follow the pure Fibonacci oscillation.
std::vector<std::string> words;
for (int i = 0; i < 100; ++i)
{
words.push_back("w" + std::to_string(i));
}
std::vector<int> expected = {1, 1, 2, 3, 5, 8, 13, 8, 5, 3, 2, 1, 1, 2, 3, 5};
for (int count : expected)
{
std::vector<std::string> line;
int placed = pattern.nextLine(words, line);
EXPECT_EQ(placed, count);
EXPECT_EQ(line.size(), static_cast<size_t>(count));
}
}
TEST(FibonacciLinePatternTest, SequencePeaksAtConfiguredWordCount)
{
// A 15-word peak (what a 1920px screen might ask for) becomes the apex of
// the oscillation instead of a hardcoded 13.
radiantwave::FibonacciLinePattern pattern(15, 100000.0f, 10.0f, widthOf);
std::vector<std::string> words;
for (int i = 0; i < 100; ++i)
{
words.push_back("w" + std::to_string(i));
}
std::vector<int> expected = {1, 1, 2, 3, 5, 8, 13, 15, 13, 8, 5, 3, 2, 1, 1, 2};
for (int count : expected)
{
std::vector<std::string> line;
EXPECT_EQ(pattern.nextLine(words, line), count);
}
}
TEST(FibonacciLinePatternTest, WidthCapTruncatesLines)
{
// Each word is 100px, space is 10px; the cap is 300px.
radiantwave::FibonacciLinePattern pattern(
13, 300.0f, 10.0f, [](const std::string &) { return 100.0f; });
std::vector<std::string> words = {"aaaa", "bbbb", "cccc", "dddd", "eeee"};
std::vector<std::string> line;
EXPECT_EQ(pattern.nextLine(words, line), 1); // target 1
line.clear();
EXPECT_EQ(pattern.nextLine(words, line), 1); // target 1
line.clear();
EXPECT_EQ(pattern.nextLine(words, line), 2); // target 2: 100 + 10 + 100 = 210 <= 300
line.clear();
// target 3: only "eeee" remains before the end of the list, so the line
// stops with one word rather than wrapping mid-line.
EXPECT_EQ(pattern.nextLine(words, line), 1);
EXPECT_EQ(line[0], "eeee");
}
TEST(FibonacciLinePatternTest, FirstWordAlwaysPlaced)
{
radiantwave::FibonacciLinePattern pattern(
13, 5.0f, 10.0f, [](const std::string &) { return 100.0f; });
std::vector<std::string> words = {"aaa", "bbb"};
std::vector<std::string> line;
EXPECT_EQ(pattern.nextLine(words, line), 1);
EXPECT_EQ(line[0], "aaa");
}
TEST(FibonacciLinePatternTest, WrapsOnlyAtLineBoundaries)
{
radiantwave::FibonacciLinePattern pattern(13, 100000.0f, 10.0f, widthOf);
std::vector<std::string> words = {"a", "b", "c"};
std::vector<std::vector<std::string>> lines;
for (int i = 0; i < 6; ++i)
{
std::vector<std::string> line;
pattern.nextLine(words, line);
lines.push_back(line);
}
// Words are consumed in order and never wrap mid-line: every line is a
// contiguous slice of "a b c a b c ...".
ASSERT_EQ(lines[0].size(), 1u);
EXPECT_EQ(lines[0][0], "a");
ASSERT_EQ(lines[1].size(), 1u);
EXPECT_EQ(lines[1][0], "b");
ASSERT_EQ(lines[2].size(), 1u); // only "c" remains; the list ends, no wrap
EXPECT_EQ(lines[2][0], "c");
ASSERT_EQ(lines[3].size(), 3u); // wraps at the line boundary instead
EXPECT_EQ(lines[3][0], "a");
EXPECT_EQ(lines[3][1], "b");
EXPECT_EQ(lines[3][2], "c");
ASSERT_EQ(lines[4].size(), 3u);
EXPECT_EQ(lines[4][0], "a");
ASSERT_EQ(lines[5].size(), 3u);
EXPECT_EQ(lines[5][0], "a");
}
TEST(FibonacciLinePatternTest, EmptyWordListYieldsZero)
{
radiantwave::FibonacciLinePattern pattern(13, 100.0f, 10.0f, widthOf);
std::vector<std::string> words;
std::vector<std::string> line;
EXPECT_EQ(pattern.nextLine(words, line), 0);
}
+293
View File
@@ -0,0 +1,293 @@
#include "scramble/fibonacci_scrambler.h"
#include <gtest/gtest.h>
#include <cstdint>
#include <unordered_map>
#include <vector>
namespace {
using radiantwave::scramble::ScrambleConfig;
using radiantwave::scramble::canvasSize;
using radiantwave::scramble::scramble;
using radiantwave::scramble::unscramble;
// W x H RGBA8888 image whose every pixel has a distinct 32-bit value, so a
// permutation can be verified by counting values. Pixel (x, y) gets the value
// y*width + x + 1; the +1 keeps every source value distinct from the
// transparent-black padding (0).
std::vector<uint8_t> uniqueImage(int width, int height)
{
std::vector<uint8_t> img(static_cast<size_t>(width) * height * 4);
for (int y = 0; y < height; ++y)
{
for (int x = 0; x < width; ++x)
{
const uint32_t v = static_cast<uint32_t>(y) * width + x + 1;
uint8_t *p = img.data() + (static_cast<size_t>(y) * width + x) * 4;
p[0] = static_cast<uint8_t>(v & 0xFF);
p[1] = static_cast<uint8_t>((v >> 8) & 0xFF);
p[2] = static_cast<uint8_t>((v >> 16) & 0xFF);
p[3] = static_cast<uint8_t>((v >> 24) & 0xFF);
}
}
return img;
}
bool roundTrips(int width, int height, int rounds, uint64_t seed)
{
const auto img = uniqueImage(width, height);
const int n = canvasSize(width, height);
std::vector<uint8_t> scrambled(static_cast<size_t>(n) * n * 4);
std::vector<uint8_t> recovered(static_cast<size_t>(width) * height * 4);
ScrambleConfig cfg;
cfg.rounds = rounds;
cfg.seed = seed;
if (!scramble(img.data(), scrambled.data(), width, height, cfg))
{
return false;
}
if (!unscramble(scrambled.data(), recovered.data(), width, height, cfg))
{
return false;
}
return recovered == img;
}
} // namespace
TEST(FibonacciScramblerTest, CanvasSizeIsMaxOfDimensions)
{
EXPECT_EQ(canvasSize(1, 1), 1);
EXPECT_EQ(canvasSize(8, 8), 8);
EXPECT_EQ(canvasSize(8, 4), 8);
EXPECT_EQ(canvasSize(4, 8), 8);
EXPECT_EQ(canvasSize(1, 8), 8);
EXPECT_EQ(canvasSize(8, 1), 8);
EXPECT_EQ(canvasSize(0, 8), 0);
EXPECT_EQ(canvasSize(8, 0), 0);
EXPECT_EQ(canvasSize(0, 0), 0);
EXPECT_EQ(canvasSize(-3, 8), 0);
}
TEST(FibonacciScramblerTest, RoundTripIsExactForSquareImages)
{
const int sizes[] = {1, 2, 3, 8, 32};
for (int n : sizes)
{
for (int t = 1; t <= 6; ++t)
{
EXPECT_TRUE(roundTrips(n, n, t, radiantwave::scramble::kDefaultSeed))
<< "N=" << n << " rounds=" << t;
}
}
}
TEST(FibonacciScramblerTest, RoundTripIsExactForWideAndTallImages)
{
const int dims[][2] = {{8, 4}, {4, 8}, {1, 8}, {8, 1}, {3, 17}, {31, 3}};
for (const auto &d : dims)
{
for (int t : {1, 4})
{
EXPECT_TRUE(roundTrips(d[0], d[1], t, radiantwave::scramble::kDefaultSeed))
<< "W=" << d[0] << " H=" << d[1] << " rounds=" << t;
}
}
}
TEST(FibonacciScramblerTest, RoundTripIsExactForGlyphShapedTiles)
{
// Punctuation glyphs are narrow or short, so N is max(W, H): these exercise
// the non-square padding path with glyph-like proportions (e.g. an
// apostrophe tile is 6 wide x 38 tall at 32px).
const int dims[][2] = {{1, 5}, {2, 7}, {6, 1}, {1, 1},
{17, 37}, {9, 37}, {37, 9}};
for (const auto &d : dims)
{
for (int t : {1, 4})
{
EXPECT_TRUE(roundTrips(d[0], d[1], t, radiantwave::scramble::kDefaultSeed))
<< "W=" << d[0] << " H=" << d[1] << " rounds=" << t;
}
}
}
TEST(FibonacciScramblerTest, ScrambleIsABijectionOnTheCanvas)
{
const int dims[][2] = {{5, 7}, {8, 8}, {3, 1}, {1, 4}};
for (const auto &d : dims)
{
const int width = d[0];
const int height = d[1];
const int n = canvasSize(width, height);
const auto img = uniqueImage(width, height);
std::vector<uint8_t> scrambled(static_cast<size_t>(n) * n * 4);
ScrambleConfig cfg;
cfg.rounds = 3;
ASSERT_TRUE(scramble(img.data(), scrambled.data(), width, height, cfg));
std::unordered_map<uint32_t, int> counts;
for (const uint8_t *p = scrambled.data(); p != scrambled.data() + scrambled.size(); p += 4)
{
const uint32_t v = static_cast<uint32_t>(p[0]) |
(static_cast<uint32_t>(p[1]) << 8) |
(static_cast<uint32_t>(p[2]) << 16) |
(static_cast<uint32_t>(p[3]) << 24);
counts[v]++;
}
// Every source pixel value appears exactly once (the map restricted to
// the W x H region is injective)...
const size_t pixels = static_cast<size_t>(width) * height;
for (size_t i = 0; i < pixels; ++i)
{
const uint32_t v = static_cast<uint32_t>(i) + 1;
EXPECT_EQ(counts[v], 1)
<< "W=" << width << " H=" << height << " value=" << v;
}
// ...and the remaining canvas positions are transparent-black padding.
EXPECT_EQ(counts[0], static_cast<int>(static_cast<size_t>(n) * n - pixels))
<< "W=" << width << " H=" << height;
}
}
TEST(FibonacciScramblerTest, ScrambleChangesTheImage)
{
const auto img = uniqueImage(8, 8);
const int n = 8;
std::vector<uint8_t> scrambled(static_cast<size_t>(n) * n * 4);
ScrambleConfig cfg;
cfg.rounds = 1;
ASSERT_TRUE(scramble(img.data(), scrambled.data(), 8, 8, cfg));
bool changed = false;
for (size_t i = 0; i < img.size(); ++i)
{
if (scrambled[i] != img[i])
{
changed = true;
break;
}
}
EXPECT_TRUE(changed);
}
TEST(FibonacciScramblerTest, OutputIsDeterministic)
{
const auto img = uniqueImage(6, 9);
const int n = 9;
std::vector<uint8_t> a(static_cast<size_t>(n) * n * 4);
std::vector<uint8_t> b(static_cast<size_t>(n) * n * 4);
ScrambleConfig cfg;
cfg.rounds = 3;
ASSERT_TRUE(scramble(img.data(), a.data(), 6, 9, cfg));
ASSERT_TRUE(scramble(img.data(), b.data(), 6, 9, cfg));
EXPECT_EQ(a, b);
}
TEST(FibonacciScramblerTest, DifferentSeedChangesOutput)
{
const auto img = uniqueImage(8, 8);
const int n = 8;
std::vector<uint8_t> a(static_cast<size_t>(n) * n * 4);
std::vector<uint8_t> b(static_cast<size_t>(n) * n * 4);
ScrambleConfig cfgA;
cfgA.rounds = 2;
cfgA.seed = radiantwave::scramble::kDefaultSeed;
ScrambleConfig cfgB = cfgA;
cfgB.seed = cfgA.seed + 1;
ASSERT_TRUE(scramble(img.data(), a.data(), 8, 8, cfgA));
ASSERT_TRUE(scramble(img.data(), b.data(), 8, 8, cfgB));
EXPECT_NE(a, b);
}
TEST(FibonacciScramblerTest, DifferentRoundsChangeOutput)
{
const auto img = uniqueImage(8, 8);
const int n = 8;
std::vector<uint8_t> a(static_cast<size_t>(n) * n * 4);
std::vector<uint8_t> b(static_cast<size_t>(n) * n * 4);
ScrambleConfig cfgA;
cfgA.rounds = 1;
ScrambleConfig cfgB;
cfgB.rounds = 2;
ASSERT_TRUE(scramble(img.data(), a.data(), 8, 8, cfgA));
ASSERT_TRUE(scramble(img.data(), b.data(), 8, 8, cfgB));
EXPECT_NE(a, b);
}
TEST(FibonacciScramblerTest, AlphaChannelIsPreservedByRoundTrip)
{
// Pixel values exercise all 32 bits, alpha included.
EXPECT_TRUE(roundTrips(11, 7, 5, 12345));
}
TEST(FibonacciScramblerTest, PaddingIsTransparentAndRoundTrips)
{
// Opaque white source: after scrambling, every canvas pixel must be either
// white (a real source pixel) or transparent black (padding), and the
// unscramble must restore pure white.
const int width = 5;
const int height = 7;
const int n = canvasSize(width, height);
std::vector<uint8_t> img(static_cast<size_t>(width) * height * 4, 255);
std::vector<uint8_t> scrambled(static_cast<size_t>(n) * n * 4);
std::vector<uint8_t> recovered(static_cast<size_t>(width) * height * 4);
ScrambleConfig cfg;
cfg.rounds = 4;
ASSERT_TRUE(scramble(img.data(), scrambled.data(), width, height, cfg));
for (size_t i = 0; i < scrambled.size(); i += 4)
{
const bool isWhite = scrambled[i] == 255 && scrambled[i + 1] == 255 &&
scrambled[i + 2] == 255 && scrambled[i + 3] == 255;
const bool isPadding = scrambled[i] == 0 && scrambled[i + 1] == 0 &&
scrambled[i + 2] == 0 && scrambled[i + 3] == 0;
EXPECT_TRUE(isWhite || isPadding)
<< "canvas pixel " << (i / 4) << " is neither white nor padding";
}
ASSERT_TRUE(unscramble(scrambled.data(), recovered.data(), width, height, cfg));
EXPECT_EQ(recovered, img);
}
TEST(FibonacciScramblerTest, RoundsBelowOneClampToOne)
{
const auto img = uniqueImage(7, 5);
const int n = 7;
std::vector<uint8_t> clamped(static_cast<size_t>(n) * n * 4);
std::vector<uint8_t> single(static_cast<size_t>(n) * n * 4);
ScrambleConfig cfgClamped;
cfgClamped.rounds = 0;
ScrambleConfig cfgSingle;
cfgSingle.rounds = 1;
ASSERT_TRUE(scramble(img.data(), clamped.data(), 7, 5, cfgClamped));
ASSERT_TRUE(scramble(img.data(), single.data(), 7, 5, cfgSingle));
EXPECT_EQ(clamped, single);
EXPECT_TRUE(roundTrips(7, 5, 0, radiantwave::scramble::kDefaultSeed));
}
TEST(FibonacciScramblerTest, RejectsInvalidArguments)
{
std::vector<uint8_t> dummy(16 * 16 * 4, 0);
ScrambleConfig cfg;
EXPECT_FALSE(scramble(nullptr, dummy.data(), 4, 4, cfg));
EXPECT_FALSE(scramble(dummy.data(), nullptr, 4, 4, cfg));
EXPECT_FALSE(scramble(dummy.data(), dummy.data(), 0, 4, cfg));
EXPECT_FALSE(scramble(dummy.data(), dummy.data(), 4, -1, cfg));
EXPECT_FALSE(unscramble(nullptr, dummy.data(), 4, 4, cfg));
EXPECT_FALSE(unscramble(dummy.data(), nullptr, 4, 4, cfg));
EXPECT_FALSE(unscramble(dummy.data(), dummy.data(), 0, 4, cfg));
}
+67
View File
@@ -0,0 +1,67 @@
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
struct TestCase {
std::string name;
void (*fn)();
};
inline std::vector<TestCase>& tests()
{
static std::vector<TestCase> t;
return t;
}
inline int register_test(const char* name, void (*fn)())
{
tests().push_back({name, fn});
return 0;
}
#define TEST(name) \
void test_##name(); \
static int reg_##name = register_test(#name, test_##name); \
void test_##name()
#define ASSERT_TRUE(expr) \
do { \
if (!(expr)) { \
std::cerr << " FAIL: " << #expr << " (" << __FILE__ << ":" \
<< __LINE__ << ")" << std::endl; \
std::exit(1); \
} \
} while (0)
#define ASSERT_FALSE(expr) ASSERT_TRUE(!(expr))
#define ASSERT_EQ(a, b) \
do { \
if ((a) != (b)) { \
std::cerr << " FAIL: " << #a << " == " << #b << " (" << __FILE__ \
<< ":" << __LINE__ << ")" << std::endl; \
std::exit(1); \
} \
} while (0)
#define RUN_ALL_TESTS() \
int main() { \
int passed = 0; \
int failed = 0; \
for (auto& tc : tests()) { \
std::cout << tc.name << "... "; \
try { \
tc.fn(); \
std::cout << "ok" << std::endl; \
passed++; \
} catch (...) { \
std::cout << "FAILED" << std::endl; \
failed++; \
} \
} \
std::cout << passed << " passed, " << failed << " failed" << std::endl;\
return failed > 0 ? 1 : 0; \
}
+140
View File
@@ -0,0 +1,140 @@
#include "test_harness.h"
#include "../src/keybind_manager.h"
// Build a SDL_KEYDOWN event for the given key + modifier set.
static SDL_Event keyDownEvent(SDL_Keycode key, SDL_Keymod mod)
{
SDL_Event ev{};
ev.type = SDL_EVENT_KEY_DOWN;
ev.key.key = key;
ev.key.mod = mod;
return ev;
}
TEST(bare_bind_fires_without_modifiers)
{
KeybindManager kb;
int calls = 0;
kb.bind(SDLK_RETURN, "enter", [&calls]() { calls++; });
ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_RETURN, SDL_KMOD_NONE)));
ASSERT_EQ(calls, 1);
}
TEST(bare_bind_ignores_modifiers)
{
KeybindManager kb;
int calls = 0;
kb.bind(SDLK_RETURN, "enter", [&calls]() { calls++; });
// A plain bind fires even while a modifier is held.
ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_RETURN, SDL_KMOD_CTRL)));
ASSERT_EQ(calls, 1);
}
TEST(combo_requires_modifier)
{
KeybindManager kb;
int calls = 0;
kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", [&calls]() { calls++; });
// Without Ctrl held the combo must not fire.
ASSERT_FALSE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_NONE)));
ASSERT_EQ(calls, 0);
// With Ctrl held it fires.
ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_CTRL)));
ASSERT_EQ(calls, 1);
}
// Regression: a physical press only sets ONE side of a paired modifier
// (e.g. SDL_KMOD_LCTRL), never the combined SDL_KMOD_CTRL mask. The raw
// bitwise check (pressed & required) == required therefore never matched and
// Ctrl+D silently did nothing on real hardware.
TEST(combo_fires_with_single_ctrl_key)
{
KeybindManager kb;
int calls = 0;
kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", [&calls]() { calls++; });
ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_LCTRL)));
ASSERT_EQ(calls, 1);
ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_RCTRL)));
ASSERT_EQ(calls, 2);
}
TEST(combo_tolerates_extra_modifiers)
{
KeybindManager kb;
int calls = 0;
kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", [&calls]() { calls++; });
// CapsLock being stuck on should not block the combo.
const SDL_Keymod extra = static_cast<SDL_Keymod>(SDL_KMOD_CTRL | SDL_KMOD_CAPS);
ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_D, extra)));
ASSERT_EQ(calls, 1);
}
TEST(combo_partial_modifiers_do_not_fire)
{
KeybindManager kb;
int calls = 0;
kb.bind(SDLK_D, static_cast<SDL_Keymod>(SDL_KMOD_CTRL | SDL_KMOD_SHIFT), "clear", [&calls]() { calls++; });
// Only Ctrl held, Shift missing: must not fire.
ASSERT_FALSE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_CTRL)));
ASSERT_EQ(calls, 0);
// Both held: fires.
ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_D, static_cast<SDL_Keymod>(SDL_KMOD_CTRL | SDL_KMOD_SHIFT))));
ASSERT_EQ(calls, 1);
}
TEST(unbind_removes_binding)
{
KeybindManager kb;
int calls = 0;
kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", [&calls]() { calls++; });
kb.unbind(SDLK_D);
ASSERT_FALSE(kb.isBound(SDLK_D));
ASSERT_FALSE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_CTRL)));
ASSERT_EQ(calls, 0);
}
TEST(unbind_all_removes_bindings)
{
KeybindManager kb;
int calls = 0;
kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", [&calls]() { calls++; });
kb.bind(SDLK_UP, "up", [&calls]() { calls++; });
kb.unbindAll();
ASSERT_FALSE(kb.isBound(SDLK_D));
ASSERT_FALSE(kb.isBound(SDLK_UP));
ASSERT_FALSE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_CTRL)));
ASSERT_EQ(calls, 0);
}
TEST(is_bound_reflects_bindings)
{
KeybindManager kb;
kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", []() {});
ASSERT_TRUE(kb.isBound(SDLK_D));
ASSERT_FALSE(kb.isBound(SDLK_UP));
}
TEST(non_keydown_event_is_ignored)
{
KeybindManager kb;
int calls = 0;
kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", [&calls]() { calls++; });
SDL_Event ev{};
ev.type = SDL_EVENT_KEY_UP;
ev.key.key = SDLK_D;
ev.key.mod = SDL_KMOD_CTRL;
ASSERT_FALSE(kb.handleEvent(ev));
ASSERT_EQ(calls, 0);
}
+181
View File
@@ -0,0 +1,181 @@
#include "network/license_service.h"
#include "db/db_keys.h"
#include <gtest/gtest.h>
#include <cstdlib>
#include <chrono>
#include <filesystem>
#include <string>
using namespace radiantwave::license;
using namespace radiantwave::db;
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 levelName = "stale";
std::string errorMessage = "stale";
EXPECT_EQ(validateStoredLicense(tmp.db, "", &levelName, &errorMessage),
Result::NotConfigured);
EXPECT_EQ(levelName, "");
EXPECT_EQ(errorMessage, "");
EXPECT_FALSE(tmp.db.hasKV(KV_SUBSCRIPTION_LEVEL_NAME));
}
TEST(LicenseServiceTest, NotConfiguredWhenNoLicense) {
TempDb tmp;
tmp.db.setKV(KV_EMAIL_ADDRESS, "user@example.com");
std::string levelName = "stale";
EXPECT_EQ(validateStoredLicense(tmp.db, "", &levelName, nullptr),
Result::NotConfigured);
EXPECT_EQ(levelName, "");
}
TEST(LicenseServiceTest, NotConfiguredWhenBothMissing) {
TempDb tmp;
EXPECT_EQ(validateStoredLicense(tmp.db), Result::NotConfigured);
}
// ─── Level helpers (no network) ─────────────────────────────────────────
TEST(LicenseServiceTest, StoredLevelIdReadsPersistedValue) {
TempDb tmp;
EXPECT_EQ(getStoredSubscriptionLevelId(tmp.db), 0);
tmp.db.setKV(KV_SUBSCRIPTION_LEVEL_ID, "2");
EXPECT_EQ(getStoredSubscriptionLevelId(tmp.db), 2);
tmp.db.setKV(KV_SUBSCRIPTION_LEVEL_ID, "not-a-number");
EXPECT_EQ(getStoredSubscriptionLevelId(tmp.db), 0);
}
TEST(LicenseServiceTest, LevelNames) {
EXPECT_EQ(subscriptionLevelName(1), "Basic");
EXPECT_EQ(subscriptionLevelName(2), "Advanced");
EXPECT_EQ(subscriptionLevelName(3), "Elite");
EXPECT_EQ(subscriptionLevelName(4), "Subscription Level 4");
}
TEST(LicenseServiceTest, LevelIdForName) {
EXPECT_EQ(subscriptionLevelIdForName("Basic"), 1);
EXPECT_EQ(subscriptionLevelIdForName("advanced"), 2);
EXPECT_EQ(subscriptionLevelIdForName("ELITE"), 3);
EXPECT_EQ(subscriptionLevelIdForName(" Elite "), 0); // unknown/whitespace
EXPECT_EQ(subscriptionLevelIdForName("Enterprise"), 0);
EXPECT_EQ(subscriptionLevelIdForName(""), 0);
}
// ─── 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));
}
// ─── Live: full validation + persistence (requires env credentials) ─────
TEST(LicenseServiceLiveTest, ValidatesAndStoresSubscriptionLevel) {
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 levelName;
std::string errorMessage;
const Result result = validateStoredLicense(tmp.db, "", &levelName, &errorMessage);
EXPECT_EQ(result, Result::Ok) << "error=" << errorMessage;
// The subscription level name (and id/description when available) must be
// persisted to the local database.
EXPECT_EQ(levelName, tmp.db.getKV(KV_SUBSCRIPTION_LEVEL_NAME));
EXPECT_FALSE(tmp.db.getKV(KV_SUBSCRIPTION_LEVEL_NAME).empty());
ASSERT_TRUE(tmp.db.hasKV(KV_SUBSCRIPTION_LEVEL_ID));
EXPECT_GT(std::atoi(tmp.db.getKV(KV_SUBSCRIPTION_LEVEL_ID).c_str()), 0);
// A successful validation must record the "last verified" stamp that drives
// the offline grace period.
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, "", nullptr, &errorMessage),
Result::Invalid);
EXPECT_FALSE(errorMessage.empty());
EXPECT_FALSE(tmp.db.hasKV(KV_SUBSCRIPTION_LEVEL_NAME));
}
+153
View File
@@ -0,0 +1,153 @@
#include "network/license_status.h"
#include "db/db_keys.h"
#include <gtest/gtest.h>
#include <cstdlib>
#include <chrono>
#include <filesystem>
#include <string>
#include <thread>
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<std::chrono::seconds>(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);
}
// ─── 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();
}
// ─── 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();
}
+58
View File
@@ -0,0 +1,58 @@
#include "logger/logger.h"
#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
using namespace radiantwave::logger;
TEST(LoggerTest, InitializeAndWriteLog) {
std::string temp_path = (std::filesystem::temp_directory_path() / "test_rw_logger1.txt").string();
if (std::filesystem::exists(temp_path)) {
std::filesystem::remove(temp_path);
}
Logger logger;
EXPECT_TRUE(logger.initialize(temp_path));
logger.info("System", "System starting up");
logger.warning("WiFi", "Network connection lost (interface wlan0 carrier dropped)");
logger.error("Database", "Database query failed (SQLITE_CORRUPT table proclamations)");
auto entries = logger.getEntries();
ASSERT_EQ(entries.size(), 3u);
EXPECT_EQ(entries[0].component, "System");
EXPECT_EQ(entries[0].level, LogLevel::Info);
EXPECT_EQ(entries[1].component, "WiFi");
EXPECT_EQ(entries[1].level, LogLevel::Warning);
EXPECT_EQ(entries[2].component, "Database");
EXPECT_EQ(entries[2].level, LogLevel::Error);
std::filesystem::remove(temp_path);
}
TEST(LoggerTest, FilterLogs) {
std::string temp_path = (std::filesystem::temp_directory_path() / "test_rw_logger2.txt").string();
if (std::filesystem::exists(temp_path)) {
std::filesystem::remove(temp_path);
}
Logger logger;
ASSERT_TRUE(logger.initialize(temp_path));
logger.debug("Core", "Debug tracing");
logger.info("Core", "Info message");
logger.warning("Core", "Warning message");
logger.error("Core", "Error message");
auto min_warn = logger.getEntriesFiltered(LogLevel::Warning, false);
ASSERT_EQ(min_warn.size(), 2u); // Warning and Error
auto exact_error = logger.getEntriesFiltered(LogLevel::Error, true);
ASSERT_EQ(exact_error.size(), 1u);
EXPECT_EQ(exact_error[0].message, "Error message");
std::filesystem::remove(temp_path);
}
+58
View File
@@ -0,0 +1,58 @@
#include "../src/page.h"
#include <string>
#include <vector>
struct LifecycleEvent {
std::string type;
int pageId;
bool operator==(const LifecycleEvent& other) const
{
return type == other.type && pageId == other.pageId;
}
bool operator!=(const LifecycleEvent& other) const
{
return !(*this == other);
}
};
inline std::vector<LifecycleEvent>& lifecycle_log()
{
static std::vector<LifecycleEvent> log;
return log;
}
inline void clear_log()
{
lifecycle_log().clear();
}
class TestPage : public Page {
public:
explicit TestPage(int id) : id_(id) {}
int id() const { return id_; }
void onPush(AppState& state) override
{
lifecycle_log().push_back({"onPush", id_});
}
void onResume(AppState& state) override
{
lifecycle_log().push_back({"onResume", id_});
}
void onPause(AppState& state) override
{
lifecycle_log().push_back({"onPause", id_});
}
void onPop(AppState& state) override
{
lifecycle_log().push_back({"onPop", id_});
}
void handleEvent(const SDL_Event& event) override {}
void update(float dt) override {}
void render(SDL_Renderer* renderer) override {}
private:
int id_;
};
+156
View File
@@ -0,0 +1,156 @@
#include "test_harness.h"
#include "test_page.h"
#include "../src/page_manager.h"
// Minimal AppState for test compilation
struct AppState {
SDL_Window* window = nullptr;
SDL_Renderer* renderer = nullptr;
PageManager pages;
};
TEST(push_single_page)
{
clear_log();
AppState state;
state.pages.push(state, std::make_unique<TestPage>(1));
ASSERT_EQ(state.pages.depth(), std::size_t(1));
ASSERT_EQ(static_cast<TestPage*>(state.pages.current())->id(), 1);
ASSERT_EQ(lifecycle_log().size(), std::size_t(1));
ASSERT_EQ(lifecycle_log()[0], (LifecycleEvent{"onPush", 1}));
}
TEST(push_two_pages)
{
clear_log();
AppState state;
state.pages.push(state, std::make_unique<TestPage>(1));
clear_log();
state.pages.push(state, std::make_unique<TestPage>(2));
ASSERT_EQ(state.pages.depth(), std::size_t(2));
ASSERT_EQ(static_cast<TestPage*>(state.pages.current())->id(), 2);
ASSERT_EQ(lifecycle_log().size(), std::size_t(2));
ASSERT_EQ(lifecycle_log()[0], (LifecycleEvent{"onPause", 1}));
ASSERT_EQ(lifecycle_log()[1], (LifecycleEvent{"onPush", 2}));
}
TEST(pop_single_page)
{
clear_log();
AppState state;
state.pages.push(state, std::make_unique<TestPage>(1));
clear_log();
state.pages.pop(state);
ASSERT_EQ(state.pages.depth(), std::size_t(0));
ASSERT_TRUE(state.pages.current() == nullptr);
ASSERT_EQ(lifecycle_log().size(), std::size_t(1));
ASSERT_EQ(lifecycle_log()[0], (LifecycleEvent{"onPop", 1}));
}
TEST(pop_resumes_previous)
{
clear_log();
AppState state;
state.pages.push(state, std::make_unique<TestPage>(1));
state.pages.push(state, std::make_unique<TestPage>(2));
clear_log();
state.pages.pop(state);
ASSERT_EQ(state.pages.depth(), std::size_t(1));
ASSERT_EQ(static_cast<TestPage*>(state.pages.current())->id(), 1);
ASSERT_EQ(lifecycle_log().size(), std::size_t(2));
ASSERT_EQ(lifecycle_log()[0], (LifecycleEvent{"onPop", 2}));
ASSERT_EQ(lifecycle_log()[1], (LifecycleEvent{"onResume", 1}));
}
TEST(replace_on_empty)
{
clear_log();
AppState state;
state.pages.replace(state, std::make_unique<TestPage>(1));
ASSERT_EQ(state.pages.depth(), std::size_t(1));
ASSERT_EQ(static_cast<TestPage*>(state.pages.current())->id(), 1);
ASSERT_EQ(lifecycle_log().size(), std::size_t(1));
ASSERT_EQ(lifecycle_log()[0], (LifecycleEvent{"onPush", 1}));
}
TEST(replace_on_existing)
{
clear_log();
AppState state;
state.pages.push(state, std::make_unique<TestPage>(1));
clear_log();
state.pages.replace(state, std::make_unique<TestPage>(2));
ASSERT_EQ(state.pages.depth(), std::size_t(1));
ASSERT_EQ(static_cast<TestPage*>(state.pages.current())->id(), 2);
ASSERT_EQ(lifecycle_log().size(), std::size_t(2));
ASSERT_EQ(lifecycle_log()[0], (LifecycleEvent{"onPop", 1}));
ASSERT_EQ(lifecycle_log()[1], (LifecycleEvent{"onPush", 2}));
}
TEST(pop_empty_stack)
{
clear_log();
AppState state;
state.pages.pop(state);
ASSERT_EQ(state.pages.depth(), std::size_t(0));
ASSERT_TRUE(state.pages.current() == nullptr);
ASSERT_EQ(lifecycle_log().size(), std::size_t(0));
}
TEST(depth_tracks_correctly)
{
AppState state;
ASSERT_EQ(state.pages.depth(), std::size_t(0));
state.pages.push(state, std::make_unique<TestPage>(1));
ASSERT_EQ(state.pages.depth(), std::size_t(1));
state.pages.push(state, std::make_unique<TestPage>(2));
ASSERT_EQ(state.pages.depth(), std::size_t(2));
state.pages.pop(state);
ASSERT_EQ(state.pages.depth(), std::size_t(1));
state.pages.pop(state);
ASSERT_EQ(state.pages.depth(), std::size_t(0));
}
TEST(push_pop_push_sequence)
{
clear_log();
AppState state;
state.pages.push(state, std::make_unique<TestPage>(1));
state.pages.push(state, std::make_unique<TestPage>(2));
state.pages.pop(state);
state.pages.push(state, std::make_unique<TestPage>(3));
ASSERT_EQ(state.pages.depth(), std::size_t(2));
ASSERT_EQ(static_cast<TestPage*>(state.pages.current())->id(), 3);
// 1: push(1), 2: push(2) [pause(1), push(2)],
// 3: pop() [pop(2), resume(1)], 4: push(3) [pause(1), push(3)]
ASSERT_EQ(lifecycle_log().size(), std::size_t(7));
ASSERT_EQ(lifecycle_log()[0], (LifecycleEvent{"onPush", 1}));
ASSERT_EQ(lifecycle_log()[1], (LifecycleEvent{"onPause", 1}));
ASSERT_EQ(lifecycle_log()[2], (LifecycleEvent{"onPush", 2}));
ASSERT_EQ(lifecycle_log()[3], (LifecycleEvent{"onPop", 2}));
ASSERT_EQ(lifecycle_log()[4], (LifecycleEvent{"onResume", 1}));
ASSERT_EQ(lifecycle_log()[5], (LifecycleEvent{"onPause", 1}));
ASSERT_EQ(lifecycle_log()[6], (LifecycleEvent{"onPush", 3}));
}
RUN_ALL_TESTS()
+95
View File
@@ -0,0 +1,95 @@
#include "network/proclamation_sync.h"
#include <gtest/gtest.h>
using namespace radiantwave::db;
using namespace radiantwave::sync;
TEST(ProclamationSyncTest, ParseProclamationsJson) {
const std::string body =
R"([{"id":"p1","name":"Peace","content":"<p>I am <b>peace</b>.</p>","categoryId":3,"solfeggioFrequencyId":2,"subscriptionLevelId":3},
{"id":"p2","name":"Joy &amp; Calm","content":"text","categoryId":0,"solfeggioFrequencyId":0},
{"notAnObject":true}])";
std::vector<ProclamationRecord> out;
EXPECT_TRUE(parseProclamationsJson(body, out));
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0].id, "p1");
EXPECT_EQ(out[0].title, "Peace");
EXPECT_EQ(out[0].content, "I am peace.");
EXPECT_EQ(out[0].category_id, 3);
EXPECT_EQ(out[0].solfeggio_frequency_id, 2);
EXPECT_EQ(out[0].required_level_id, 3);
EXPECT_EQ(out[1].id, "p2");
EXPECT_EQ(out[1].title, "Joy & Calm");
EXPECT_EQ(out[1].content, "text");
EXPECT_EQ(out[1].category_id, 0);
// Missing subscriptionLevelId defaults to the least privileged level.
EXPECT_EQ(out[1].required_level_id, 1);
}
TEST(ProclamationSyncTest, ParseLevelIdDefaultsToBasic) {
std::vector<ProclamationRecord> out;
EXPECT_TRUE(parseProclamationsJson(
R"([{"id":"a","name":"A","subscriptionLevelId":0},
{"id":"b","name":"B","subscriptionLevelId":2}])", out));
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0].required_level_id, 1);
EXPECT_EQ(out[1].required_level_id, 2);
}
TEST(ProclamationSyncTest, ParseCategoriesJson) {
const std::string body =
R"([{"id":3,"name":"Spiritual","parentId":0,"sortOrder":1},
{"id":7,"name":"Inner","parentId":3,"sortOrder":2}])";
std::vector<ProclamationCategoryRecord> out;
EXPECT_TRUE(parseCategoriesJson(body, out));
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0].id, 3);
EXPECT_EQ(out[0].name, "Spiritual");
EXPECT_EQ(out[0].parent_id, 0);
EXPECT_EQ(out[0].sort_order, 1);
EXPECT_EQ(out[1].id, 7);
EXPECT_EQ(out[1].name, "Inner");
EXPECT_EQ(out[1].parent_id, 3);
EXPECT_EQ(out[1].sort_order, 2);
}
TEST(ProclamationSyncTest, RejectMalformedJson) {
std::vector<ProclamationRecord> procs;
std::vector<ProclamationCategoryRecord> cats;
EXPECT_FALSE(parseProclamationsJson("not json", procs));
EXPECT_FALSE(parseProclamationsJson(R"({"id":"p1"})", procs)); // object, not array
EXPECT_FALSE(parseProclamationsJson(R"([{"id":"p1"}", ])", procs));
EXPECT_FALSE(parseCategoriesJson("garbage", cats));
// Non-numeric ids are tolerated (row skipped), not fatal.
EXPECT_TRUE(parseCategoriesJson(R"([{"id":"x"}])", cats));
EXPECT_EQ(cats.size(), 0u);
}
TEST(ProclamationSyncTest, ParseSkipsMissingIdAndZeroCategory) {
std::vector<ProclamationRecord> procs;
EXPECT_TRUE(parseProclamationsJson(R"([{"name":"NoId"},{"id":"ok","name":"Fine","categoryId":4}])", procs));
ASSERT_EQ(procs.size(), 1u);
EXPECT_EQ(procs[0].id, "ok");
std::vector<ProclamationCategoryRecord> cats;
EXPECT_TRUE(parseCategoriesJson(R"([{"id":0,"name":"Bad"},{"id":9,"name":"Good","parentId":0}])", cats));
ASSERT_EQ(cats.size(), 1u);
EXPECT_EQ(cats[0].id, 9);
}
TEST(ProclamationSyncTest, StripHtmlEntities) {
std::vector<ProclamationRecord> out;
const std::string body =
R"([{"id":"p1","name":"N","content":"A &amp; B &lt;C&gt; &#39;quoted&#39; &nbsp;end"}] )";
EXPECT_TRUE(parseProclamationsJson(body, out));
ASSERT_EQ(out.size(), 1u);
EXPECT_EQ(out[0].content, "A & B <C> 'quoted' end");
}
+51
View File
@@ -0,0 +1,51 @@
#include "test_harness.h"
#include "../src/provisioning.h"
using radiantwave::provisioning::firstMissingStep;
using radiantwave::provisioning::indicatesFirstRun;
using radiantwave::provisioning::Step;
TEST(first_missing_step_all_satisfied)
{
ASSERT_EQ(firstMissingStep(true, true, true), Step::Done);
}
TEST(first_missing_step_no_internet)
{
// Internet is checked before email/license regardless of the others.
ASSERT_EQ(firstMissingStep(false, false, false), Step::Internet);
ASSERT_EQ(firstMissingStep(false, true, true), Step::Internet);
}
TEST(first_missing_step_no_email)
{
ASSERT_EQ(firstMissingStep(true, false, false), Step::Email);
ASSERT_EQ(firstMissingStep(true, false, true), Step::Email);
}
TEST(first_missing_step_no_license)
{
ASSERT_EQ(firstMissingStep(true, true, false), Step::License);
}
TEST(first_run_all_missing)
{
ASSERT_TRUE(indicatesFirstRun(false, false, false));
}
TEST(first_run_has_internet)
{
ASSERT_FALSE(indicatesFirstRun(true, false, false));
}
TEST(first_run_has_email)
{
ASSERT_FALSE(indicatesFirstRun(false, true, false));
}
TEST(first_run_has_license)
{
ASSERT_FALSE(indicatesFirstRun(false, false, true));
}
RUN_ALL_TESTS()
+165
View File
@@ -0,0 +1,165 @@
#include "scroller_streams.h"
#include "db/database_manager.h"
#include <gtest/gtest.h>
#include <string>
#include <unordered_map>
#include <vector>
namespace {
using radiantwave::db::AudioFileRecord;
using radiantwave::db::ProclamationRecord;
using radiantwave::scroller::ProclamationStream;
using radiantwave::scroller::StreamColor;
using radiantwave::scroller::buildStreams;
using radiantwave::scroller::kDefaultRepeatCount;
using radiantwave::scroller::kSelectedRepeatCount;
const std::string kDefaultId = "default";
std::vector<StreamColor> palette()
{
StreamColor c;
c.r = 10;
c.g = 20;
c.b = 30;
return {c};
}
} // namespace
TEST(ScrollerStreamsTest, InterleavesSelectedWithDefault)
{
ProclamationRecord p1;
p1.id = "one";
p1.title = "One";
p1.content = "hello world";
p1.selected = true;
p1.solfeggio_frequency_id = 2;
ProclamationRecord p2;
p2.id = "two";
p2.title = "Two";
p2.content = "foo bar baz";
p2.selected = true;
p2.solfeggio_frequency_id = 5;
ProclamationRecord def;
def.id = kDefaultId;
def.title = "Default";
def.content = "default text";
def.selected = true;
def.solfeggio_frequency_id = 5;
std::unordered_map<int, std::vector<StreamColor>> palettes;
palettes[2] = palette();
std::unordered_map<int, AudioFileRecord> audio;
AudioFileRecord af;
af.id = 1;
af.filename = "396Hz.wav";
af.solfeggio_frequency_id = 2;
audio[2] = af;
auto streams = buildStreams({p1, p2, def}, palettes, palette(), audio, kDefaultId);
// The first user-selected proclamation plays first, followed by the
// default, then the next selected, then the default again.
ASSERT_EQ(streams.size(), 4u);
EXPECT_EQ(streams[0].name, "One");
ASSERT_EQ(streams[0].words.size(), 2u);
EXPECT_EQ(streams[0].words[0], "hello");
EXPECT_EQ(streams[0].words[1], "world");
EXPECT_EQ(streams[0].repeat, kSelectedRepeatCount);
EXPECT_EQ(streams[0].solfeggio_frequency_id, 2);
EXPECT_EQ(streams[0].audioFilename, "396Hz.wav");
EXPECT_EQ(streams[0].palette.size(), 1u);
EXPECT_EQ(streams[1].name, "Default");
EXPECT_EQ(streams[1].repeat, kDefaultRepeatCount);
EXPECT_EQ(streams[1].solfeggio_frequency_id, 5);
EXPECT_EQ(streams[1].audioFilename, "OriginalAudio.wav"); // no audio for freq 5
EXPECT_EQ(streams[1].palette.size(), 1u); // falls back to 528 palette
EXPECT_EQ(streams[2].name, "Two");
EXPECT_EQ(streams[2].repeat, kSelectedRepeatCount);
EXPECT_EQ(streams[2].palette.size(), 1u); // falls back to 528 palette
EXPECT_EQ(streams[3].name, "Default");
EXPECT_EQ(streams[3].repeat, kDefaultRepeatCount);
EXPECT_EQ(streams[3].audioFilename, "OriginalAudio.wav");
}
TEST(ScrollerStreamsTest, DefaultOnlyWhenNothingSelected)
{
ProclamationRecord p1;
p1.id = "one";
p1.title = "One";
p1.content = "x";
p1.selected = false;
ProclamationRecord def;
def.id = kDefaultId;
def.title = "Default";
def.content = "y";
def.selected = true;
auto streams = buildStreams({p1, def}, {}, palette(), {}, kDefaultId);
ASSERT_EQ(streams.size(), 1u);
EXPECT_EQ(streams[0].name, "Default");
EXPECT_EQ(streams[0].repeat, kDefaultRepeatCount);
}
TEST(ScrollerStreamsTest, MissingDefaultStillStreamsSelected)
{
ProclamationRecord p1;
p1.id = "one";
p1.title = "One";
p1.content = "x";
p1.selected = true;
auto streams = buildStreams({p1}, {}, palette(), {}, kDefaultId);
ASSERT_EQ(streams.size(), 1u);
EXPECT_EQ(streams[0].name, "One");
EXPECT_EQ(streams[0].repeat, kSelectedRepeatCount);
}
TEST(ScrollerStreamsTest, SplitsGluedSentencePunctuation)
{
ProclamationRecord p1;
p1.id = "one";
p1.title = "One";
p1.content = "state?Create NOW?KING field’s fine (Acts 17:27)I example.com";
p1.selected = true;
auto streams = buildStreams({p1}, {}, palette(), {}, kDefaultId);
ASSERT_EQ(streams.size(), 1u);
const auto& words = streams[0].words;
// "?" glued to an uppercase word splits after the punctuation, keeping the
// sentence separator attached to the preceding word.
EXPECT_EQ(words[0], "state?");
EXPECT_EQ(words[1], "Create");
EXPECT_EQ(words[2], "NOW?");
EXPECT_EQ(words[3], "KING");
// Curly apostrophes are not sentence punctuation and stay inside the word.
EXPECT_EQ(words[4], "field’s");
EXPECT_EQ(words[5], "fine");
// Closing paren followed by an uppercase letter splits so the verse
// reference and the next word are separate tokens.
EXPECT_EQ(words[6], "(Acts");
EXPECT_EQ(words[7], "17:27)");
EXPECT_EQ(words[8], "I");
// A lowercase continuation after a dot is not a new sentence, so this
// stays intact.
EXPECT_EQ(words[9], "example.com");
}
+39
View File
@@ -0,0 +1,39 @@
#include "timezone_helper.h"
#include <gtest/gtest.h>
#include <ctime>
using namespace radiantwave::timezone;
TEST(TimeZoneTest, GetStandardTimeZonesNonEmpty) {
const auto& countries = getStandardTimeZones();
EXPECT_FALSE(countries.empty());
bool found_us = false;
bool found_utc = false;
for (const auto& c : countries) {
if (c.country_name == "United States") found_us = true;
if (c.country_name == "Coordinated Universal Time") found_utc = true;
}
EXPECT_TRUE(found_us);
EXPECT_TRUE(found_utc);
}
TEST(TimeZoneTest, UtcTimestampFormat) {
std::string iso = getUtcTimestampIso();
EXPECT_FALSE(iso.empty());
EXPECT_NE(iso.find("UTC"), std::string::npos);
}
TEST(TimeZoneTest, FormatUtcToTimezone) {
// 2026-07-24 12:00:00 UTC
// Fixed epoch timestamp: 1784808000
time_t fixed_utc = 1784808000;
std::string formatted_ny = formatUtcToTimezone(fixed_utc, "America/New_York", "%Y-%m-%d %H:%M:%S");
std::string formatted_utc = formatUtcToTimezone(fixed_utc, "UTC", "%Y-%m-%d %H:%M:%S");
EXPECT_FALSE(formatted_ny.empty());
EXPECT_EQ(formatted_utc, "2026-07-23 12:00:00");
}
+45
View File
@@ -0,0 +1,45 @@
#include "validation.h"
#include <gtest/gtest.h>
using namespace radiantwave::validation;
TEST(ValidationTest, ValidEmails) {
EXPECT_TRUE(isValidEmail("user@example.com"));
EXPECT_TRUE(isValidEmail("first.last@example.co.uk"));
EXPECT_TRUE(isValidEmail("user+tag@example.com"));
EXPECT_TRUE(isValidEmail("UPPER.CASE@EXAMPLE.ORG"));
EXPECT_TRUE(isValidEmail("a@b.co"));
}
TEST(ValidationTest, InvalidEmails) {
EXPECT_FALSE(isValidEmail(""));
EXPECT_FALSE(isValidEmail("ab"));
EXPECT_FALSE(isValidEmail("user@"));
EXPECT_FALSE(isValidEmail("@example.com"));
EXPECT_FALSE(isValidEmail("user@@example.com"));
EXPECT_FALSE(isValidEmail("user@example")); // no dot in domain
EXPECT_FALSE(isValidEmail("user name@example.com")); // whitespace
EXPECT_FALSE(isValidEmail("user@.example.com")); // empty label
EXPECT_FALSE(isValidEmail("user@example..com")); // empty label
EXPECT_FALSE(isValidEmail(".user@example.com")); // leading dot in local
EXPECT_FALSE(isValidEmail("user.@example.com")); // trailing dot in local
EXPECT_FALSE(isValidEmail(std::string(255, 'a') + "@example.com"));
}
TEST(ValidationTest, SanitizeLicenseKey) {
EXPECT_EQ(sanitizeLicenseKey(""), "");
EXPECT_EQ(sanitizeLicenseKey("abcd-efgh-1234-5678"), "ABCDEFGH12345678");
EXPECT_EQ(sanitizeLicenseKey("ab cd ef gh ij kl"), "ABCDEFGHIJKL"); // whitespace stripped
EXPECT_EQ(sanitizeLicenseKey("AbC1!!dEf"), "ABC1DEF"); // symbols stripped
EXPECT_EQ(sanitizeLicenseKey("12345678901234567890"), "1234567890123456"); // capped at 16
}
TEST(ValidationTest, FormatLicenseKey) {
EXPECT_EQ(formatLicenseKey(""), "");
EXPECT_EQ(formatLicenseKey("ABCD"), "ABCD");
EXPECT_EQ(formatLicenseKey("ABCDEFGH"), "ABCD-EFGH");
EXPECT_EQ(formatLicenseKey("ABCDEFGH12345678"), "ABCD-EFGH-1234-5678");
// Grouping applies to whatever is passed; length capping is the
// sanitizer's responsibility.
EXPECT_EQ(formatLicenseKey("ABCDEFGH123456789"), "ABCD-EFGH-1234-5678-9");
}