Remove subscription levels; license valid = subscribed

This commit is contained in:
NeilClack
2026-08-15 00:54:24 -05:00
parent 8ca60de647
commit f14bf3e29a
19 changed files with 136 additions and 1534 deletions
+8 -40
View File
@@ -72,19 +72,19 @@ TEST(ApiClientTest, ParseFrequenciesJsonSkipsMissingId) {
EXPECT_EQ(out[0].id, 3);
}
TEST(ApiClientTest, ParseLicenseJsonLegacyStringLevel) {
TEST(ApiClientTest, ParseLicenseJson) {
const std::string body =
R"({"key":"ABCD-EFGH-IJKL-MNOP","email_address":"user@example.com","subscription_level":"Basic"})";
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");
EXPECT_EQ(info.subscription_level.name, "Basic");
EXPECT_EQ(info.subscription_level.id, 0);
}
TEST(ApiClientTest, ParseLicenseJsonObjectLevel) {
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"}})";
@@ -93,22 +93,6 @@ TEST(ApiClientTest, ParseLicenseJsonObjectLevel) {
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) {
@@ -147,7 +131,7 @@ TEST(ApiClientTest, ExtractErrorMessage) {
// ─── Offline: real-world payload shapes ────────────────────────────────
TEST(ApiClientTest, ParseProclamationsHandlesSubscriptionLevelIdNull) {
TEST(ApiClientTest, ParseProclamationsToleratesLegacyLevelField) {
const std::string body =
R"([{"id":"a","name":"Peace","content":"<p>I am peace.</p>","categoryId":5,
"solfeggioFrequencyId":5,"subscriptionLevelId":null},
@@ -256,33 +240,17 @@ TEST(ApiClientLiveTest, ValidateLicenseSuccessLive) {
<< " 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());
AccountResult result = createAccount("", "", 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) ────────
@@ -292,7 +260,7 @@ TEST(ApiClientLiveTest, CreateAccountSuccessLive) {
GTEST_SKIP() << "RW_LIVE_ACCOUNT_EMAIL not set";
}
AccountResult result = createAccount(email, "Basic", "RadiantWave Test", liveApiKey());
AccountResult result = createAccount(email, "RadiantWave Test", liveApiKey());
ASSERT_TRUE(result.ok()) << "status=" << result.status
<< " error=" << result.error_message;
EXPECT_TRUE(result.body.success);
-26
View File
@@ -142,32 +142,6 @@ TEST(DatabaseManagerTest, SyncProclamationsPreservesSelectionAndDeletesRemoved)
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");
+9 -50
View File
@@ -47,23 +47,18 @@ 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),
EXPECT_EQ(validateStoredLicense(tmp.db, "", &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),
EXPECT_EQ(validateStoredLicense(tmp.db, "", nullptr),
Result::NotConfigured);
EXPECT_EQ(levelName, "");
}
TEST(LicenseServiceTest, NotConfiguredWhenBothMissing) {
@@ -71,35 +66,6 @@ TEST(LicenseServiceTest, NotConfiguredWhenBothMissing) {
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) {
@@ -139,7 +105,7 @@ TEST(LicenseServiceTest, GracePeriodIgnoresCorruptStamp) {
// ─── Live: full validation + persistence (requires env credentials) ─────
TEST(LicenseServiceLiveTest, ValidatesAndStoresSubscriptionLevel) {
TEST(LicenseServiceLiveTest, ValidatesAndGrantsSubscribedState) {
const std::string key = env("RW_LIVE_LICENSE_KEY");
const std::string email = env("RW_LIVE_LICENSE_EMAIL");
if (key.empty() || email.empty()) {
@@ -150,20 +116,14 @@ TEST(LicenseServiceLiveTest, ValidatesAndStoresSubscriptionLevel) {
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);
const Result result = validateStoredLicense(tmp.db, "", &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.
// A successful validation is the "subscribed" flag; no subscription-level
// state is persisted. Only the "last verified" stamp that drives the
// offline grace window is written.
EXPECT_FALSE(tmp.db.hasKV("subscription_level_name"));
EXPECT_TRUE(tmp.db.hasKV(KV_LICENSE_LAST_VERIFIED));
EXPECT_TRUE(isLicenseWithinGracePeriod(tmp.db));
}
@@ -174,8 +134,7 @@ TEST(LicenseServiceLiveTest, InvalidPairReportsInvalid) {
tmp.db.setKV(KV_EMAIL_ADDRESS, "nobody@example.com");
std::string errorMessage;
EXPECT_EQ(validateStoredLicense(tmp.db, "", nullptr, &errorMessage),
EXPECT_EQ(validateStoredLicense(tmp.db, "", &errorMessage),
Result::Invalid);
EXPECT_FALSE(errorMessage.empty());
EXPECT_FALSE(tmp.db.hasKV(KV_SUBSCRIPTION_LEVEL_NAME));
}
+5 -7
View File
@@ -6,7 +6,7 @@ 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},
R"([{"id":"p1","name":"Peace","content":"<p>I am <b>peace</b>.</p>","categoryId":3,"solfeggioFrequencyId":2},
{"id":"p2","name":"Joy &amp; Calm","content":"text","categoryId":0,"solfeggioFrequencyId":0},
{"notAnObject":true}])";
@@ -19,24 +19,22 @@ TEST(ProclamationSyncTest, ParseProclamationsJson) {
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);
EXPECT_EQ(out[1].solfeggio_frequency_id, 0);
}
TEST(ProclamationSyncTest, ParseLevelIdDefaultsToBasic) {
TEST(ProclamationSyncTest, ParseIgnoresLegacySubscriptionLevelField) {
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);
EXPECT_EQ(out[0].id, "a");
EXPECT_EQ(out[1].id, "b");
}
TEST(ProclamationSyncTest, ParseCategoriesJson) {