Warning screen - New reusable WarningPage (word-wrapped message, ENTER acknowledges and continues to the screen you were about to enter). - First use: pressing Play while the selected audio is still downloading. Dynamic Audio requires every file; a fixed track requires its own file. License re-validation - No longer re-verifies on every Home render. Checks now run only when the email/license key is entered or changed, on the manual option, at startup when already due, and hourly while valid. - Invalid + unchanged credentials are never re-checked (persisted pair). - Valid re-checks at most once per hour, anchored on a DB timestamp so an overdue check runs immediately after a power cycle. - New system_kv keys track the checked pair, outcome, and last-check time. - Added a System Settings row "Manually Validate License Key OK|Invalid" (OK green, Invalid red, label white) that runs a check immediately. Portal / local integration - License validation request now sends the app version as `appVersion`. - The entered license key is mirrored to ~/.local/radiantwave/license.txt. Misc - "Please connect to WiFi" only shows when no WiFi is joined AND the internet is unreachable (fixes the NetworkManager-missing and Ethernet cases). - Added unit tests for the audio-download warning, license scheduling policy, license request shape, and the new settings-menu entry.
220 lines
8.0 KiB
C++
220 lines
8.0 KiB
C++
#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")), "");
|
|
}
|
|
|
|
TEST(AudioDownloadWarning, DynamicAudioWarnsWhenAnyFileMissing) {
|
|
AudioDownloadSnapshot snap;
|
|
snap.manifestKnown = true;
|
|
snap.allAvailable = false;
|
|
snap.files = {
|
|
{"174Hz.wav", "174 Hz", true},
|
|
{"396Hz.wav", "396 Hz", false},
|
|
};
|
|
std::string message;
|
|
ASSERT_TRUE(buildAudioDownloadWarning(snap, true, "396Hz.wav", message));
|
|
EXPECT_NE(message.find("'Dynamic Audio'"), std::string::npos);
|
|
EXPECT_NE(message.find("these files"), std::string::npos);
|
|
}
|
|
|
|
TEST(AudioDownloadWarning, DynamicAudioSilentWhenAllAvailable) {
|
|
AudioDownloadSnapshot snap;
|
|
snap.manifestKnown = true;
|
|
snap.allAvailable = true;
|
|
snap.files = {{"174Hz.wav", "174 Hz", true}};
|
|
std::string message;
|
|
EXPECT_FALSE(buildAudioDownloadWarning(snap, true, "174Hz.wav", message));
|
|
}
|
|
|
|
TEST(AudioDownloadWarning, FixedTrackWarnsWhenItsFileMissing) {
|
|
AudioDownloadSnapshot snap;
|
|
snap.manifestKnown = true;
|
|
snap.allAvailable = false;
|
|
snap.files = {
|
|
{"174Hz.wav", "174 Hz", true},
|
|
{"OriginalAudio.wav", "Original Audio", false},
|
|
};
|
|
std::string message;
|
|
ASSERT_TRUE(buildAudioDownloadWarning(snap, false, "OriginalAudio.wav", message));
|
|
EXPECT_NE(message.find("'Original Audio'"), std::string::npos);
|
|
EXPECT_NE(message.find("this file"), std::string::npos);
|
|
}
|
|
|
|
TEST(AudioDownloadWarning, FixedTrackSilentWhenItsFilePresentEvenIfOthersMissing) {
|
|
AudioDownloadSnapshot snap;
|
|
snap.manifestKnown = true;
|
|
snap.allAvailable = false;
|
|
snap.files = {
|
|
{"174Hz.wav", "174 Hz", true},
|
|
{"396Hz.wav", "396 Hz", false},
|
|
};
|
|
std::string message;
|
|
EXPECT_FALSE(buildAudioDownloadWarning(snap, false, "174Hz.wav", message));
|
|
}
|
|
|
|
TEST(AudioDownloadWarning, UnknownSelectionNeverWarns) {
|
|
// No manifest fetched yet: the downloader has no evidence either way.
|
|
AudioDownloadSnapshot snap;
|
|
std::string message;
|
|
EXPECT_FALSE(buildAudioDownloadWarning(snap, false, "OriginalAudio.wav", message));
|
|
}
|
|
|
|
TEST(AudioDownloadWarning, FallsBackToFilenameForDisplayName) {
|
|
AudioDownloadSnapshot snap;
|
|
snap.manifestKnown = true;
|
|
snap.files = {{"Mystery.wav", "", false}};
|
|
std::string message;
|
|
ASSERT_TRUE(buildAudioDownloadWarning(snap, false, "Mystery.wav", message));
|
|
EXPECT_NE(message.find("'Mystery.wav'"), std::string::npos);
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace radiantwave::audio
|