#include "network/api_client.h" #include "network/json.h" #include "network/proclamation_sync.h" #include "db/database_manager.h" #include #include #include #include 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: 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 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 out; EXPECT_TRUE(parseFrequenciesJson("[]", out)); EXPECT_TRUE(out.empty()); } TEST(ApiClientTest, ParseFrequenciesJsonRejectsMalformed) { std::vector 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 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, ParseLicenseJson) { const std::string body = R"({"key":"ABCD-EFGH-IJKL-MNOP","email_address":"user@example.com"})"; LicenseInfo info; EXPECT_TRUE(parseLicenseJson(body, info)); EXPECT_EQ(info.key, "ABCD-EFGH-IJKL-MNOP"); EXPECT_EQ(info.email_address, "user@example.com"); } TEST(ApiClientTest, ParseLicenseJsonIgnoresSubscriptionFields) { // The active/inactive model has no subscription level. Legacy responses // that still carry subscription_level must parse; the field is ignored. 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"); } 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)); } TEST(ApiClientTest, ParseScheduleJson) { const std::string body = R"({"email":"jane@example.com","blocks":[ {"id":"b1","startTime":"06:00","startMinutes":360,"endTime":"08:00", "endMinutes":480,"overnight":false,"durationMinutes":120, "dynamicAudio":false,"proclamationIds":["p1","p2"]}, {"id":"b2","startTime":"21:00","startMinutes":1260,"endTime":"09:00", "endMinutes":540,"overnight":true,"durationMinutes":720, "dynamicAudio":true,"proclamationIds":[]}]})"; ScheduleResponse out; EXPECT_TRUE(parseScheduleJson(body, out)); EXPECT_EQ(out.email, "jane@example.com"); ASSERT_EQ(out.blocks.size(), 2u); EXPECT_EQ(out.blocks[0].id, "b1"); EXPECT_EQ(out.blocks[0].start_minutes, 360); EXPECT_EQ(out.blocks[0].end_minutes, 480); EXPECT_FALSE(out.blocks[0].overnight); EXPECT_EQ(out.blocks[0].duration_minutes, 120); EXPECT_FALSE(out.blocks[0].dynamic_audio); ASSERT_EQ(out.blocks[0].proclamation_ids.size(), 2u); EXPECT_EQ(out.blocks[0].proclamation_ids[0], "p1"); EXPECT_EQ(out.blocks[0].proclamation_ids[1], "p2"); EXPECT_EQ(out.blocks[1].id, "b2"); EXPECT_TRUE(out.blocks[1].overnight); EXPECT_TRUE(out.blocks[1].dynamic_audio); EXPECT_TRUE(out.blocks[1].proclamation_ids.empty()); } TEST(ApiClientTest, ParseScheduleDefaultsDynamicAudioTrue) { const std::string body = R"({"email":"a@b.com","blocks":[ {"id":"b1","startMinutes":0,"endMinutes":60,"overnight":false, "durationMinutes":60,"proclamationIds":["p1"]}]})"; ScheduleResponse out; EXPECT_TRUE(parseScheduleJson(body, out)); ASSERT_EQ(out.blocks.size(), 1u); EXPECT_TRUE(out.blocks[0].dynamic_audio); } TEST(ApiClientTest, ParseScheduleRejectsMalformed) { ScheduleResponse out; EXPECT_FALSE(parseScheduleJson("not json", out)); EXPECT_FALSE(parseScheduleJson("[1,2,3]", out)); // array, not object // Missing id blocks are skipped, not fatal. EXPECT_TRUE(parseScheduleJson(R"({"blocks":[{"startMinutes":0}]})", out)); EXPECT_TRUE(out.blocks.empty()); } // ─── 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, ParseProclamationsToleratesLegacyLevelField) { const std::string body = R"([{"id":"a","name":"Peace","content":"

I am peace.

","categoryId":5, "solfeggioFrequencyId":5,"subscriptionLevelId":null}, {"id":"b","name":"Foot","content":"

Heal.

","categoryId":32, "solfeggioFrequencyId":7,"subscriptionLevelId":1}])"; std::vector 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, ParseFlatCategoriesAndGroups) { const std::string categoriesBody = R"([{"id":4,"name":"Spiritual","sortOrder":0}, {"id":5,"name":"General","sortOrder":1}])"; std::vector categories; EXPECT_TRUE(radiantwave::sync::parseCategoriesJson(categoriesBody, categories)); ASSERT_EQ(categories.size(), 2u); EXPECT_EQ(categories[0].id, 4); EXPECT_EQ(categories[1].id, 5); const std::string groupsBody = R"([{"id":9,"name":"Prayers","sortOrder":0}])"; std::vector groups; EXPECT_TRUE(radiantwave::sync::parseGroupsJson(groupsBody, groups)); ASSERT_EQ(groups.size(), 1u); EXPECT_EQ(groups[0].id, 9); EXPECT_EQ(groups[0].name, "Prayers"); } // ─── Live: GET /frequencies (harmless, always runs) ─────────────────── TEST(ApiClientLiveTest, FetchFrequenciesLive) { std::vector out; EXPECT_TRUE(fetchFrequencies(out)); 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); ASSERT_TRUE(procsResponse.transportOk); ASSERT_EQ(procsResponse.status, 200); std::vector 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); ASSERT_TRUE(catsResponse.transportOk); ASSERT_EQ(catsResponse.status, 200); std::vector categories; EXPECT_TRUE(radiantwave::sync::parseCategoriesJson(catsResponse.body, categories)); EXPECT_FALSE(categories.empty()); HttpResponse groupsResponse = httpGet(PATH_GROUPS); ASSERT_TRUE(groupsResponse.transportOk); ASSERT_EQ(groupsResponse.status, 200); std::vector groups; EXPECT_TRUE(radiantwave::sync::parseGroupsJson(groupsResponse.body, groups)); EXPECT_FALSE(groups.empty()); } // ─── Live: POST /license error paths (no credentials required) ───────── TEST(ApiClientLiveTest, ValidateLicenseMissingKeyLive) { LicenseResult result = validateLicense("", "test@example.com"); 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", ""); 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"); 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); ASSERT_TRUE(result.ok()) << "status=" << result.status << " error=" << result.error_message; EXPECT_EQ(result.info.key, key); EXPECT_EQ(result.info.email_address, email); } // ─── Live: POST /automations/create/account error paths (harmless) ───── TEST(ApiClientLiveTest, CreateAccountMissingEmailLive) { AccountResult result = createAccount("", ""); ASSERT_FALSE(result.ok()); EXPECT_EQ(result.status, 400); EXPECT_EQ(result.error_message, "email is required"); } // ─── 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, "RadiantWave Test"); 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()); }