Compare commits

...
33 Commits
Author SHA1 Message Date
MrMirDan 051f4ca6ef removed external add_executable 2026-10-16 11:08:49 +03:00
MrMirDan 69d87e70ac some changes for apiCountryListModel test 2026-10-16 10:46:27 +03:00
MrMirDan b7ea6c498d added apiCountryListModel test 2026-10-15 16:25:51 +03:00
MrMirDan 8ad00d533e changed api news and devices tests 2026-10-15 16:18:13 +03:00
MrMirDan b4c5c381ba trying to fix error C1075 2026-10-15 14:47:23 +03:00
MrMirDan 3c25dfce55 some changes for build 2026-10-15 14:04:01 +03:00
MrMirDan d8fa1f5161 changed api tests after rebase 2026-10-15 12:57:31 +03:00
MrMirDan 6960287ef5 fixed syntax error 2026-10-15 11:28:55 +03:00
MrMirDan 1386336f10 changed some expected values for services model and controller test 2026-10-15 11:28:55 +03:00
MrMirDan c482fa51a7 removed testApiDevicesModel's testRolesNames 2026-10-15 11:28:55 +03:00
MrMirDan f49fa1ac60 added apiDevicesModel test 2026-10-15 11:28:54 +03:00
MrMirDan 34101a91d9 added apiCountryModel test 2026-10-15 11:28:42 +03:00
MrMirDan 972b4e1274 added apiBenefitsModel test 2026-10-15 11:28:28 +03:00
MrMirDan df47fdcae0 changed some expected values 2026-10-15 11:28:13 +03:00
MrMirDan bc5ae6e410 fixed typo in naming 2026-10-15 11:27:12 +03:00
MrMirDan 0d38b22162 added apiAccountInfoModel test 2026-10-15 11:24:31 +03:00
MrMirDan 4773fb0821 services test little changes 2026-10-15 11:24:17 +03:00
MrMirDan a91e1a20f0 subscription controller test changes 2026-10-15 11:24:17 +03:00
MrMirDan 24b74fcb3e controller fix 2026-10-15 11:24:17 +03:00
MrMirDan ed3deef2dd added subscriptionUiController test 2026-10-15 11:24:15 +03:00
MrMirDan 185c85e183 some service test changes 2026-10-15 11:23:54 +03:00
MrMirDan bb7cf69cfc removed unused test 2026-10-15 11:23:54 +03:00
MrMirDan 5ba704ffcf returned some dependencies 2026-10-15 11:23:54 +03:00
MrMirDan 1b47d79f26 some fixes for news test 2026-10-15 11:23:54 +03:00
MrMirDan a5cb5679ae temporary checks 2026-10-15 11:23:54 +03:00
MrMirDan 6aefc6be47 various changes 2026-10-15 11:23:03 +03:00
MrMirDan 378b30f553 fix: removed last arg for makeServiceItem 2026-10-15 11:23:03 +03:00
MrMirDan 812ee905f4 fix: proper object name in services tests 2026-10-15 11:23:03 +03:00
MrMirDan b16d603435 changed prem_key to amnezia_test_api_key 2026-10-15 11:23:03 +03:00
MrMirDan c107c01cab rewrited news test to use gateway and added services test to cmake and workflow 2026-10-15 11:23:01 +03:00
MrMirDan 80ff2e0676 added ui services model and controller api test 2026-10-15 11:22:04 +03:00
MrMirDan 4b6f09d55f fix: typos 2026-10-15 11:22:04 +03:00
MrMirDan c6958d8b70 added news ui test 2026-10-15 11:21:36 +03:00
11 changed files with 1625 additions and 141 deletions

No files matched your search

+28 -1
View File
@@ -1057,6 +1057,14 @@ jobs:
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
- UiNewsModelAndControllerTest
- UiServicesModelAndControllerTest
- UiSubscriptionControllerTest
- UiApiAccountInfoModelTest
- ApiBenefitsModelTest
- ApiCountryModelTest
- ApiCountryListModelTest
- ApiDevicesModelTest
runs-on: ubuntu-22.04
@@ -1077,6 +1085,7 @@ jobs:
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
steps:
- name: Checkout
@@ -1247,6 +1256,14 @@ jobs:
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
- UiNewsModelAndControllerTest
- UiServicesModelAndControllerTest
- UiSubscriptionControllerTest
- UiApiAccountInfoModelTest
- ApiBenefitsModelTest
- ApiCountryModelTest
- ApiCountryListModelTest
- ApiDevicesModelTest
runs-on: windows-latest
@@ -1266,7 +1283,8 @@ jobs:
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}|
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
steps:
- name: Checkout
@@ -1354,6 +1372,14 @@ jobs:
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
- UiNewsModelAndControllerTest
- UiServicesModelAndControllerTest
- UiSubscriptionControllerTest
- UiApiAccountInfoModelTest
- ApiBenefitsModelTest
- ApiCountryModelTest
- ApiCountryListModelTest
- ApiDevicesModelTest
runs-on: macos-15
@@ -1374,6 +1400,7 @@ jobs:
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
steps:
- name: Setup Xcode
+71 -10
View File
@@ -1,12 +1,3 @@
add_executable(test_ui_api_services_model_and_controller
testUiApiServicesModelAndController.cpp
)
target_link_libraries(test_ui_api_services_model_and_controller PRIVATE
Qt6::Test
test_common
)
add_executable(test_ui_news_model_and_controller
testUiNewsModelAndController.cpp
)
@@ -25,8 +16,78 @@ target_link_libraries(test_gateway_service_import PRIVATE
test_common
)
add_test(NAME UiApiServicesModelAndControllerTest COMMAND test_ui_api_services_model_and_controller)
add_executable(test_ui_services_model_and_controller
testUiServicesModelAndController.cpp
)
target_link_libraries(test_ui_services_model_and_controller PRIVATE
Qt6::Test
test_common
)
add_executable(test_ui_subscription_controller
testSubscriptionUiController.cpp
)
target_link_libraries(test_ui_subscription_controller PRIVATE
Qt6::Test
test_common
)
add_executable(test_ui_api_account_info_model
testUiApiAccountInfoModel.cpp
)
target_link_libraries(test_ui_api_account_info_model PRIVATE
Qt6::Test
test_common
)
add_executable(test_api_benefits_model
testApiBenefitsModel.cpp
)
target_link_libraries(test_api_benefits_model PRIVATE
Qt6::Test
test_common
)
add_executable(test_api_country_model
testApiCountryModel.cpp
)
target_link_libraries(test_api_country_model PRIVATE
Qt6::Test
test_common
)
add_executable(test_api_country_list_model
testApiCountryListModel.cpp
${CLIENT_ROOT_DIR}/resources/countries.qrc
)
target_link_libraries(test_api_country_list_model PRIVATE
Qt6::Test
test_common
)
add_executable(test_api_devices_model
testApiDevicesModel.cpp
)
target_link_libraries(test_api_devices_model PRIVATE
Qt6::Test
test_common
)
add_test(NAME UiNewsModelAndControllerTest COMMAND test_ui_news_model_and_controller)
add_test(NAME GatewayServiceImportTest COMMAND test_gateway_service_import)
add_test(NAME UiServicesModelAndControllerTest COMMAND test_ui_services_model_and_controller)
add_test(NAME UiSubscriptionControllerTest COMMAND test_ui_subscription_controller)
add_test(NAME UiApiAccountInfoModelTest COMMAND test_ui_api_account_info_model)
add_test(NAME ApiBenefitsModelTest COMMAND test_api_benefits_model)
add_test(NAME ApiCountryModelTest COMMAND test_api_country_model)
add_test(NAME ApiCountryListModelTest COMMAND test_api_country_list_model)
add_test(NAME ApiDevicesModelTest COMMAND test_api_devices_model)
amnezia_copy_runtime_dlls()
+213
View File
@@ -0,0 +1,213 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QSignalSpy>
#include <QTest>
#include "ui/models/api/apiBenefitsModel.h"
class TestApiBenefitsModel : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void initialState();
void updateModel();
void updateModelClearsOnEmptyArray();
void updateModelSkipsInvalidItems();
void updateModelSkipsItemsWithoutTitleAndBody();
void updateModelTrimsLinkedBody();
void updateModelPreservesQrcIcon();
void updateModelUsesFallbackIcon();
void dataReturnsInvalidForInvalidIndex();
void dataReturnsInvalidForUnknownRole();
void roleNames();
void clear();
};
void TestApiBenefitsModel::initTestCase()
{
qRegisterMetaType<QModelIndex>();
}
void TestApiBenefitsModel::initialState()
{
ApiBenefitsModel model;
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.rowCount(QModelIndex()), 0);
}
void TestApiBenefitsModel::updateModel()
{
ApiBenefitsModel model;
const QJsonArray benefits = { QJsonObject { { "icon", "globe-2" },
{ "title", "Global access" },
{ "body", "Access servers worldwide" },
{ "link", false } },
QJsonObject { { "icon", "smartphone" },
{ "title", "Multiple devices" },
{ "body", "Connect your devices" },
{ "link", true } } };
QSignalSpy aboutToResetSpy(&model, &QAbstractItemModel::modelAboutToBeReset);
QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset);
model.updateModel(benefits);
QCOMPARE(aboutToResetSpy.count(), 1);
QCOMPARE(resetSpy.count(), 1);
QCOMPARE(model.rowCount(), 2);
const QModelIndex firstIndex = model.index(0, 0);
QVERIFY(firstIndex.isValid());
QCOMPARE(model.rowCount(firstIndex), 0);
QCOMPARE(model.data(firstIndex, ApiBenefitsModel::IconRole).toString(), QStringLiteral("qrc:/images/controls/globe-2.svg"));
QCOMPARE(model.data(firstIndex, ApiBenefitsModel::TitleRole).toString(), QStringLiteral("Global access"));
QCOMPARE(model.data(firstIndex, ApiBenefitsModel::BodyRole).toString(), QStringLiteral("Access servers worldwide"));
QCOMPARE(model.data(firstIndex, ApiBenefitsModel::LinkRole).toBool(), false);
const QModelIndex secondIndex = model.index(1, 0);
QVERIFY(secondIndex.isValid());
QCOMPARE(model.data(secondIndex, ApiBenefitsModel::IconRole).toString(), QStringLiteral("qrc:/images/controls/smartphone.svg"));
QCOMPARE(model.data(secondIndex, ApiBenefitsModel::TitleRole).toString(), QStringLiteral("Multiple devices"));
QCOMPARE(model.data(secondIndex, ApiBenefitsModel::BodyRole).toString(), QStringLiteral("Connect your devices"));
QCOMPARE(model.data(secondIndex, ApiBenefitsModel::LinkRole).toBool(), true);
}
void TestApiBenefitsModel::updateModelClearsOnEmptyArray()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" } } });
QCOMPARE(model.rowCount(), 1);
QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset);
model.updateModel(QJsonArray { });
QCOMPARE(resetSpy.count(), 1);
QCOMPARE(model.rowCount(), 0);
}
void TestApiBenefitsModel::updateModelSkipsInvalidItems()
{
ApiBenefitsModel model;
const QJsonArray benefits = { QJsonObject { { "title", "Valid benefit" }, { "body", "Valid description" } },
QJsonValue(QStringLiteral("invalid item")), QJsonValue(42), QJsonValue(false),
QJsonValue(QJsonArray { }) };
model.updateModel(benefits);
QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::TitleRole).toString(), QStringLiteral("Valid benefit"));
}
void TestApiBenefitsModel::updateModelSkipsItemsWithoutTitleAndBody()
{
ApiBenefitsModel model;
const QJsonArray benefits = { QJsonObject { }, QJsonObject { { "title", "" }, { "body", "" } },
QJsonObject { { "title", "Title only" } }, QJsonObject { { "body", "Body only" } } };
model.updateModel(benefits);
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::TitleRole).toString(), QStringLiteral("Title only"));
QCOMPARE(model.data(model.index(1, 0), ApiBenefitsModel::BodyRole).toString(), QStringLiteral("Body only"));
}
void TestApiBenefitsModel::updateModelTrimsLinkedBody()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Link" }, { "body", " https://example.com \n" }, { "link", true } },
QJsonObject { { "title", "Text" }, { "body", " Description " }, { "link", false } } });
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::BodyRole).toString(), QStringLiteral("https://example.com"));
QCOMPARE(model.data(model.index(1, 0), ApiBenefitsModel::BodyRole).toString(), QStringLiteral(" Description "));
}
void TestApiBenefitsModel::updateModelPreservesQrcIcon()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" }, { "icon", "qrc:/custom/icon.svg" } } });
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::IconRole).toString(), QStringLiteral("qrc:/custom/icon.svg"));
}
void TestApiBenefitsModel::updateModelUsesFallbackIcon()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" }, { "icon", "unknown-icon" } } });
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::IconRole).toString(), QStringLiteral("qrc:/images/controls/info.svg"));
}
void TestApiBenefitsModel::dataReturnsInvalidForInvalidIndex()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" } } });
QVERIFY(!model.data(QModelIndex(), ApiBenefitsModel::TitleRole).isValid());
QVERIFY(!model.data(model.index(-1, 0), ApiBenefitsModel::TitleRole).isValid());
QVERIFY(!model.data(model.index(1, 0), ApiBenefitsModel::TitleRole).isValid());
QVERIFY(!model.data(model.index(0, 1), ApiBenefitsModel::TitleRole).isValid());
}
void TestApiBenefitsModel::dataReturnsInvalidForUnknownRole()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" } } });
const QModelIndex index = model.index(0, 0);
QVERIFY(!model.data(index, Qt::DisplayRole).isValid());
QVERIFY(!model.data(index, ApiBenefitsModel::LinkRole + 1).isValid());
}
void TestApiBenefitsModel::roleNames()
{
ApiBenefitsModel model;
const QHash<int, QByteArray> roles = model.roleNames();
QCOMPARE(roles.value(ApiBenefitsModel::IconRole), QByteArray("icon"));
QCOMPARE(roles.value(ApiBenefitsModel::TitleRole), QByteArray("title"));
QCOMPARE(roles.value(ApiBenefitsModel::BodyRole), QByteArray("body"));
QCOMPARE(roles.value(ApiBenefitsModel::LinkRole), QByteArray("link"));
}
void TestApiBenefitsModel::clear()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Benefit 1" }, { "body", "Description 1" } },
QJsonObject { { "title", "Benefit 2" }, { "body", "Description 2" } } });
QSignalSpy aboutToResetSpy(&model, &QAbstractItemModel::modelAboutToBeReset);
QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset);
model.clear();
QCOMPARE(aboutToResetSpy.count(), 1);
QCOMPARE(resetSpy.count(), 1);
QCOMPARE(model.rowCount(), 0);
model.clear();
QCOMPARE(model.rowCount(), 0);
QCOMPARE(resetSpy.count(), 2);
}
QTEST_MAIN(TestApiBenefitsModel)
#include "testApiBenefitsModel.moc"
@@ -0,0 +1,165 @@
#include <QSignalSpy>
#include <QTest>
#include "ui/models/api/apiCountryListModel.h"
class TestApiCountryListModel : public QObject
{
Q_OBJECT
private slots:
void initialState();
void dataReturnsInvalidForInvalidIndex();
void searchTextChanges();
void searchTextChangedSignal();
void clearSearch();
void sortModeChanges();
void invalidSortModeFallsBackToByRegion();
void favoritesAreSortedAndDeduplicated();
void collapsedSectionsAreStored();
void sanitizeSearchFieldText();
};
void TestApiCountryListModel::initialState()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.rowCount(QModelIndex()), 0);
QVERIFY(!model.hasResults());
QVERIFY(!model.isSearchActive());
QVERIFY(model.searchText().isEmpty());
QCOMPARE(model.sortMode(), ApiCountryListModel::ByRegion);
QVERIFY(model.isGrouped());
QCOMPARE(model.listId(), QStringLiteral("test"));
QCOMPARE(model.activeUseCaseId(), QStringLiteral("all"));
QCOMPARE(model.favoritesLimit(), 15);
}
void TestApiCountryListModel::dataReturnsInvalidForInvalidIndex()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
QVERIFY(!model.data(QModelIndex(), Qt::DisplayRole).isValid());
QVERIFY(!model.data(model.index(-1, 0), ApiCountryListModel::CountryNameRole).isValid());
QVERIFY(!model.data(model.index(0, 0), ApiCountryListModel::CountryNameRole).isValid());
}
void TestApiCountryListModel::searchTextChanges()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
model.setSearchText(QStringLiteral("Estonia"));
QCOMPARE(model.searchText(), QStringLiteral("Estonia"));
QVERIFY(model.isSearchActive());
QCOMPARE(model.rowCount(), 0);
QVERIFY(!model.hasResults());
model.setSearchText(QStringLiteral(" "));
QVERIFY(!model.isSearchActive());
QCOMPARE(model.searchText(), QStringLiteral(" "));
}
void TestApiCountryListModel::searchTextChangedSignal()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
QSignalSpy spy(&model, &ApiCountryListModel::searchTextChanged);
QVERIFY(spy.isValid());
model.setSearchText(QStringLiteral("Estonia"));
QCOMPARE(spy.count(), 1);
model.setSearchText(QStringLiteral("Estonia"));
QCOMPARE(spy.count(), 1);
model.setSearchText(QStringLiteral("Latvia"));
QCOMPARE(spy.count(), 2);
}
void TestApiCountryListModel::clearSearch()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
model.setSearchText(QStringLiteral("Estonia"));
QVERIFY(model.isSearchActive());
QSignalSpy spy(&model, &ApiCountryListModel::searchTextChanged);
QVERIFY(spy.isValid());
model.clearSearch();
QVERIFY(model.searchText().isEmpty());
QVERIFY(!model.isSearchActive());
QCOMPARE(spy.count(), 1);
model.clearSearch();
QCOMPARE(spy.count(), 1);
}
void TestApiCountryListModel::sortModeChanges()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
QSignalSpy sortSpy(&model, &ApiCountryListModel::sortModeChanged);
QSignalSpy groupingSpy(&model, &ApiCountryListModel::groupingChanged);
QVERIFY(sortSpy.isValid());
QVERIFY(groupingSpy.isValid());
model.setSortMode(ApiCountryListModel::Alphabetical);
QCOMPARE(model.sortMode(), ApiCountryListModel::Alphabetical);
QVERIFY(!model.isGrouped());
QCOMPARE(sortSpy.count(), 1);
QCOMPARE(groupingSpy.count(), 1);
model.setSortMode(ApiCountryListModel::Alphabetical);
QCOMPARE(sortSpy.count(), 1);
QCOMPARE(groupingSpy.count(), 1);
}
void TestApiCountryListModel::invalidSortModeFallsBackToByRegion()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
model.setSortMode(ApiCountryListModel::Alphabetical);
model.setSortMode(999);
QCOMPARE(model.sortMode(), ApiCountryListModel::ByRegion);
QVERIFY(model.isGrouped());
}
void TestApiCountryListModel::favoritesAreSortedAndDeduplicated()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
model.setFavorites({ QStringLiteral("US"), QStringLiteral("DE"), QStringLiteral("US"), QString() });
QCOMPARE(model.favorites(), QStringList({ QStringLiteral("DE"), QStringLiteral("US") }));
}
void TestApiCountryListModel::collapsedSectionsAreStored()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
model.setCollapsedSections({ QStringLiteral("europe"), QStringLiteral("asia"), QStringLiteral("europe"), QString() });
QCOMPARE(model.collapsedSections(), QStringList({ QStringLiteral("asia"), QStringLiteral("europe") }));
}
void TestApiCountryListModel::sanitizeSearchFieldText()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
QCOMPARE(model.sanitizeSearchFieldText(QStringLiteral("Estonia")), QStringLiteral("Estonia"));
QCOMPARE(model.sanitizeSearchFieldText(QStringLiteral("Hello, World! 123")), QStringLiteral("Hello World "));
QCOMPARE(model.sanitizeSearchFieldText(QStringLiteral("")), QString());
QCOMPARE(model.sanitizeSearchFieldText(QStringLiteral("123")), QString());
}
QTEST_MAIN(TestApiCountryListModel)
#include "testApiCountryListModel.moc"
+190
View File
@@ -0,0 +1,190 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QSignalSpy>
#include <QTest>
#include "core/utils/constants/apiKeys.h"
#include "ui/models/api/apiCountryModel.h"
class TestApiCountryModel : public QObject
{
Q_OBJECT
private:
ApiCountryModel m_model;
static QJsonObject makeCountry(const QString &name, const QString &code, const QString &localizedCode)
{
return { { apiDefs::key::serverCountryName, name },
{ apiDefs::key::serverCountryCode, code },
{ apiDefs::key::serverCountryCodeL10n, localizedCode } };
}
static QJsonObject makeIssuedConfig(const QString &countryCode, const QString &sourceType,
const QString &lastDownloaded, const QString &workerLastUpdated)
{
return { { apiDefs::key::serverCountryCode, countryCode },
{ apiDefs::key::sourceType, sourceType },
{ apiDefs::key::lastDownloaded, lastDownloaded },
{ apiDefs::key::workerLastUpdated, workerLastUpdated },
{ apiDefs::key::installationUuid, "test-installation" },
{ apiDefs::key::osVersion, "test-os" } };
}
private slots:
void init()
{
m_model.updateIssuedConfigsInfo(QJsonArray {});
m_model.updateModel(QJsonArray {}, QString {}, QString {});
}
void testInitialState()
{
QCOMPARE(m_model.rowCount(), 0);
QCOMPARE(m_model.getCurrentIndex(), -1);
QVERIFY(m_model.getCurrentCountryCode().isEmpty());
QVERIFY(m_model.getUserCountryCode().isEmpty());
QVERIFY(!m_model.hasExpiredWorkerConfigs());
QVERIFY(!m_model.data(m_model.index(0, 0), ApiCountryModel::CountryNameRole).isValid());
}
void testUpdateModel()
{
const QJsonArray countries { makeCountry("Germany", "DE", "de"), makeCountry("Estonia", "EE", "ee"),
makeCountry("United States", "US", "us") };
QSignalSpy currentIndexSpy(&m_model, &ApiCountryModel::currentIndexChanged);
m_model.updateModel(countries, "EE", "us");
QCOMPARE(m_model.rowCount(), 3);
QCOMPARE(m_model.getCurrentIndex(), 1);
QCOMPARE(m_model.getCurrentCountryCode(), QString("EE"));
QCOMPARE(m_model.getUserCountryCode(), QString("US"));
QCOMPARE(currentIndexSpy.count(), 1);
QCOMPARE(currentIndexSpy.constFirst().constFirst().toInt(), 1);
const QModelIndex germanyIndex = m_model.index(0, 0);
const QModelIndex estoniaIndex = m_model.index(1, 0);
const QModelIndex unitedStatesIndex = m_model.index(2, 0);
QVERIFY(germanyIndex.isValid());
QCOMPARE(m_model.data(germanyIndex, ApiCountryModel::CountryNameRole).toString(), QString("Germany"));
QCOMPARE(m_model.data(germanyIndex, ApiCountryModel::CountryCodeRole).toString(), QString("DE"));
QCOMPARE(m_model.data(estoniaIndex, ApiCountryModel::CountryNameRole).toString(), QString("Estonia"));
QCOMPARE(m_model.data(estoniaIndex, ApiCountryModel::CountryCodeRole).toString(), QString("EE"));
QCOMPARE(m_model.data(unitedStatesIndex, ApiCountryModel::CountryNameRole).toString(), QString("United States"));
QCOMPARE(m_model.data(unitedStatesIndex, ApiCountryModel::CountryCodeRole).toString(), QString("US"));
}
void testCurrentCountryNotFound()
{
const QJsonArray countries { makeCountry("Germany", "DE", "de"), makeCountry("Estonia", "EE", "ee") };
m_model.updateModel(countries, "FI", "ee");
QCOMPARE(m_model.rowCount(), 2);
QCOMPARE(m_model.getCurrentIndex(), -1);
QCOMPARE(m_model.getCurrentCountryCode(), QString("FI"));
QCOMPARE(m_model.getUserCountryCode(), QString("EE"));
}
void testCountryImageCodeRole()
{
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
const QModelIndex index = m_model.index(0, 0);
QCOMPARE(m_model.data(index, ApiCountryModel::CountryImageCodeRole).toString(), QString("EE"));
}
void testCountryImageCodeFallback()
{
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "") }, "EE", "EE");
QCOMPARE(m_model.data(m_model.index(0, 0), ApiCountryModel::CountryImageCodeRole).toString(), QString("EE"));
}
void testInvalidIndexesAndRoles()
{
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
QVERIFY(!m_model.data(QModelIndex(), ApiCountryModel::CountryNameRole).isValid());
QVERIFY(!m_model.data(m_model.index(-1, 0), ApiCountryModel::CountryNameRole).isValid());
QVERIFY(!m_model.data(m_model.index(1, 0), ApiCountryModel::CountryNameRole).isValid());
QVERIFY(!m_model.data(m_model.index(0, 0), Qt::UserRole + 100).isValid());
}
void testUpdateIssuedConfigsInfo()
{
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee"), makeCountry("Germany", "DE", "de") }, "EE", "EE");
const QJsonArray issuedConfigs {
// Worker timestamp equals download timestamp: not expired.
makeIssuedConfig("EE", "country_config", "2026-10-10T10:00:00", "2026-10-10T10:00:00"),
// Worker timestamp is newer than the download timestamp: expired.
makeIssuedConfig("DE", "country_config", "2026-10-09T10:00:00", "2026-10-10T10:00:00"),
// Non-country configurations must be ignored.
makeIssuedConfig("EE", "other_config", "2026-10-01T10:00:00", "2026-10-20T10:00:00")
};
m_model.updateIssuedConfigsInfo(issuedConfigs);
const QModelIndex estoniaIndex = m_model.index(0, 0);
const QModelIndex germanyIndex = m_model.index(1, 0);
QVERIFY(m_model.data(estoniaIndex, ApiCountryModel::IsIssuedRole).toBool());
QVERIFY(!m_model.data(estoniaIndex, ApiCountryModel::IsWorkerExpiredRole).toBool());
QVERIFY(m_model.data(germanyIndex, ApiCountryModel::IsIssuedRole).toBool());
QVERIFY(m_model.data(germanyIndex, ApiCountryModel::IsWorkerExpiredRole).toBool());
QVERIFY(m_model.hasExpiredWorkerConfigs());
}
void testNonCountryConfigIsIgnored()
{
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
m_model.updateIssuedConfigsInfo(QJsonArray { makeIssuedConfig("EE", "other_config", "2026-10-01T10:00:00", "2026-10-20T10:00:00") });
const QModelIndex index = m_model.index(0, 0);
QVERIFY(!m_model.data(index, ApiCountryModel::IsIssuedRole).toBool());
QVERIFY(!m_model.data(index, ApiCountryModel::IsWorkerExpiredRole).toBool());
QVERIFY(!m_model.hasExpiredWorkerConfigs());
}
void testCountryWithoutIssuedConfig()
{
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
m_model.updateIssuedConfigsInfo(QJsonArray { });
const QModelIndex index = m_model.index(0, 0);
QVERIFY(!m_model.data(index, ApiCountryModel::IsIssuedRole).toBool());
QVERIFY(!m_model.data(index, ApiCountryModel::IsWorkerExpiredRole).toBool());
QVERIFY(!m_model.hasExpiredWorkerConfigs());
}
void testUpdateModelResetsCountries()
{
m_model.updateModel(QJsonArray { makeCountry("Germany", "DE", "de"), makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
m_model.updateModel(QJsonArray { makeCountry("Finland", "FI", "fi") }, "FI", "FI");
QCOMPARE(m_model.rowCount(), 1);
QCOMPARE(m_model.getCurrentIndex(), 0);
QCOMPARE(m_model.getCurrentCountryCode(), QString("FI"));
QCOMPARE(m_model.getUserCountryCode(), QString("FI"));
QCOMPARE(m_model.data(m_model.index(0, 0), ApiCountryModel::CountryNameRole).toString(), QString("Finland"));
}
};
QTEST_MAIN(TestApiCountryModel)
#include "testApiCountryModel.moc"
+158
View File
@@ -0,0 +1,158 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QSignalSpy>
#include <QTest>
#include "core/utils/constants/apiKeys.h"
#include "ui/models/api/apiDevicesModel.h"
class TestApiDevicesModel : public QObject
{
Q_OBJECT
private:
static QJsonObject createDevice(const QString &installationUuid, const QString &sourceType,
const QString &osVersion, const QString &countryCode, const QString &lastDownloaded)
{
return { { apiDefs::key::installationUuid, installationUuid },
{ apiDefs::key::sourceType, sourceType },
{ apiDefs::key::osVersion, osVersion },
{ apiDefs::key::serverCountryCode, countryCode },
{ apiDefs::key::lastDownloaded, lastDownloaded },
{ apiDefs::key::workerLastUpdated, QStringLiteral("2026-10-01T12:00:00Z") } };
}
private slots:
void testEmptyModel()
{
ApiDevicesModel model;
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.rowCount(QModelIndex()), 0);
const QJsonArray devices { createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"),
QStringLiteral("Windows 11"), QStringLiteral("EE"),
QStringLiteral("2026-10-10T12:00:00Z")) };
model.updateModel(devices, QStringLiteral("device-1"));
QCOMPARE(model.rowCount(), 1);
const QModelIndex index = model.index(0, 0);
QVERIFY(index.isValid());
}
void testUpdateModel()
{
ApiDevicesModel model;
const QString currentUuid = QStringLiteral("device-1");
QJsonArray devices {
createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")),
createDevice(QStringLiteral("device-2"), QStringLiteral("gateway_account"), QStringLiteral("Android 15"), QStringLiteral("DE"), QStringLiteral("2026-10-11T12:00:00Z"))
};
model.updateModel(devices, currentUuid);
QCOMPARE(model.rowCount(), 2);
const QModelIndex firstIndex = model.index(0, 0);
const QModelIndex secondIndex = model.index(1, 0);
QVERIFY(firstIndex.isValid());
QVERIFY(secondIndex.isValid());
QCOMPARE(model.data(firstIndex, ApiDevicesModel::OsVersionRole).toString(), QStringLiteral("Windows 11"));
QCOMPARE(model.data(firstIndex, ApiDevicesModel::SupportTagRole).toString(), QStringLiteral("device-1"));
QCOMPARE(model.data(firstIndex, ApiDevicesModel::CountryCodeRole).toString(), QStringLiteral("EE"));
QCOMPARE(model.data(secondIndex, ApiDevicesModel::OsVersionRole).toString(), QStringLiteral("Android 15"));
QCOMPARE(model.data(secondIndex, ApiDevicesModel::SupportTagRole).toString(), QStringLiteral("device-2"));
QCOMPARE(model.data(secondIndex, ApiDevicesModel::CountryCodeRole).toString(), QStringLiteral("DE"));
}
void testCurrentDeviceRole()
{
ApiDevicesModel model;
QJsonArray devices {
createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")),
createDevice(QStringLiteral("device-2"), QStringLiteral("gateway_account"), QStringLiteral("Android 15"), QStringLiteral("DE"), QStringLiteral("2026-10-11T12:00:00Z"))
};
model.updateModel(devices, QStringLiteral("device-2"));
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.data(model.index(0, 0), ApiDevicesModel::IsCurrentDeviceRole).toBool(), false);
QCOMPARE(model.data(model.index(1, 0), ApiDevicesModel::IsCurrentDeviceRole).toBool(), true);
}
void testLastUpdateRole()
{
ApiDevicesModel model;
const QString lastDownloaded = QStringLiteral("2026-10-10T12:00:00Z");
model.updateModel(QJsonArray { createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), lastDownloaded) }, QStringLiteral("device-1"));
const QModelIndex index = model.index(0, 0);
QVERIFY(index.isValid());
const QString expected = QDateTime::fromString(lastDownloaded, Qt::ISODate).toLocalTime().toString(QStringLiteral("d MMM yyyy"));
QCOMPARE(model.data(index, ApiDevicesModel::LastUpdateRole).toString(), expected);
}
void testFiltersNonGatewayDevices()
{
ApiDevicesModel model;
QJsonArray devices {
createDevice(QStringLiteral("gateway-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")),
createDevice(QStringLiteral("other-1"), QStringLiteral("other_source"), QStringLiteral("Linux"), QStringLiteral("DE"), QStringLiteral("2026-10-11T12:00:00Z")),
createDevice(QStringLiteral("gateway-2"), QStringLiteral("gateway_account"), QStringLiteral("Android 15"), QStringLiteral("FI"), QStringLiteral("2026-10-12T12:00:00Z"))
};
model.updateModel(devices, QStringLiteral("gateway-2"));
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.data(model.index(0, 0), ApiDevicesModel::SupportTagRole).toString(), QStringLiteral("gateway-1"));
QCOMPARE(model.data(model.index(1, 0), ApiDevicesModel::SupportTagRole).toString(), QStringLiteral("gateway-2"));
QCOMPARE(model.data(model.index(1, 0), ApiDevicesModel::IsCurrentDeviceRole).toBool(), true);
}
void testEmptyUpdateClearsModel()
{
ApiDevicesModel model;
QJsonArray devices { createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")) };
model.updateModel(devices, QStringLiteral("device-1"));
QCOMPARE(model.rowCount(), 1);
model.updateModel(QJsonArray { }, QString { });
QCOMPARE(model.rowCount(), 0);
}
void testInvalidIndexes()
{
ApiDevicesModel model;
const QModelIndex invalidIndex;
QVERIFY(!model.data(invalidIndex, ApiDevicesModel::OsVersionRole).isValid());
QJsonArray devices { createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")) };
model.updateModel(devices, QStringLiteral("device-1"));
QVERIFY(!model.data(model.index(0, 1), ApiDevicesModel::OsVersionRole).isValid());
QVERIFY(!model.data(model.index(-1, 0), ApiDevicesModel::OsVersionRole).isValid());
QVERIFY(!model.data(model.index(1, 0), ApiDevicesModel::OsVersionRole).isValid());
QVERIFY(!model.data(model.index(0, 0), Qt::DisplayRole).isValid());
}
};
QTEST_MAIN(TestApiDevicesModel)
#include "testApiDevicesModel.moc"
@@ -0,0 +1,108 @@
#include <QSignalSpy>
#include <QTest>
#include <QUuid>
#include "utils/testCoreController.h"
#include "utils/testUtils.h"
#include "secureQSettings.h"
#include "ui/controllers/api/subscriptionUiController.h"
#include "vpnConnection.h"
using namespace amnezia;
class TestSubscriptionUiController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController = nullptr;
SecureQSettings *m_settings = nullptr;
SubscriptionUiController *m_subscriptionUiController = nullptr;
private slots:
void initTestCase()
{
const QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
const auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
m_subscriptionUiController = m_coreController->findChild<SubscriptionUiController *>();
QVERIFY2(m_subscriptionUiController != nullptr, "SubscriptionUiController should be created by CoreController");
}
void cleanupTestCase()
{
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
m_subscriptionUiController = nullptr;
}
void init()
{
m_settings->clearSettings();
QVERIFY2(m_subscriptionUiController != nullptr, "SubscriptionUiController should be available");
}
void testInitialState()
{
QCOMPARE(m_subscriptionUiController->property("qrCodesCount").toInt(), 0);
QCOMPARE(m_subscriptionUiController->property("qrCodes").toList().size(), 0);
QVERIFY(m_subscriptionUiController->property("vpnKey").toString().isEmpty());
QVERIFY(!m_subscriptionUiController->isCaptchaAwaitingUser());
}
void testExportVpnKeyWithEmptyFileName()
{
QSignalSpy errorSpy(m_subscriptionUiController, &SubscriptionUiController::errorOccurred);
QVERIFY(errorSpy.isValid());
const bool result = m_subscriptionUiController->exportVpnKey(QStringLiteral("unknown-server"), QString());
QVERIFY2(!result, "Export should fail when the file name is empty");
QCOMPARE(errorSpy.count(), 1);
QCOMPARE(m_subscriptionUiController->property("vpnKey").toString(), QString());
QCOMPARE(m_subscriptionUiController->property("qrCodesCount").toInt(), 0);
}
void testGetRenewalLinkWithEmptyServerId()
{
QSignalSpy errorSpy(m_subscriptionUiController, &SubscriptionUiController::errorOccurred);
QVERIFY(errorSpy.isValid());
m_subscriptionUiController->getRenewalLink(QString());
QCOMPARE(errorSpy.count(), 1);
QCOMPARE(errorSpy.at(0).at(0).value<ErrorCode>(), ErrorCode::InternalError);
}
void testCurrentActivePlanInfoWithoutActivePurchase()
{
const QVariantMap info = m_subscriptionUiController->currentActivePlanInfo();
QVERIFY(info.contains(QStringLiteral("hasActivePlan")));
QVERIFY2(!info.value(QStringLiteral("hasActivePlan")).toBool(), "No active store purchase should be reported");
}
void testErrorSignalIsAvailable()
{
QSignalSpy errorSpy(m_subscriptionUiController, &SubscriptionUiController::errorOccurred);
QVERIFY(errorSpy.isValid());
QCOMPARE(errorSpy.count(), 0);
}
};
QTEST_MAIN(TestSubscriptionUiController)
#include "testSubscriptionUiController.moc"
@@ -0,0 +1,146 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QSignalSpy>
#include <QTest>
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/configKeys.h"
#include "ui/models/api/apiAccountInfoModel.h"
using namespace amnezia;
namespace
{
QJsonObject makeAccountInfo()
{
return { { apiDefs::key::activeDeviceCount, 2 },
{ apiDefs::key::maxDeviceCount, 5 },
{ apiDefs::key::subscriptionEndDate, "2099-12-31T23:59:59Z" },
{ apiDefs::key::subscriptionDescription, "Premium subscription" },
{ apiDefs::key::isRenewalAvailable, true },
{ apiDefs::key::availableCountries, QJsonArray { "ru", "de" } },
{ apiDefs::key::issuedConfigs, QJsonArray { } },
{ apiDefs::key::supportInfo,
QJsonObject { { apiDefs::key::telegram, "https://t.me/test_support" },
{ apiDefs::key::email, "support@example.org" },
{ apiDefs::key::billingEmail, "billing@example.org" },
{ apiDefs::key::websiteName, "Example" },
{ apiDefs::key::website, "https://example.org" } } } };
}
QJsonObject makePremiumServerConfig()
{
QJsonObject apiConfig;
apiConfig[apiDefs::key::isInAppPurchase] = false;
apiConfig[apiDefs::key::serviceType] = "amnezia-premium";
QJsonObject config;
config[configKey::configVersion] = serverConfigUtils::ConfigSource::AmneziaGateway;
config[apiDefs::key::apiConfig] = apiConfig;
return config;
}
} // namespace
class TestUiApiAccountInfoModel : public QObject
{
Q_OBJECT
private slots:
void testInitialState()
{
ApiAccountInfoModel model;
QCOMPARE(model.rowCount(), 1);
QVERIFY(!model.data(QModelIndex(), ApiAccountInfoModel::SubscriptionStatusRole).isValid());
const QModelIndex invalidIndex = model.index(1, 0);
QVERIFY(!model.data(invalidIndex, ApiAccountInfoModel::SubscriptionStatusRole).isValid());
}
void testUpdateModel()
{
ApiAccountInfoModel model;
QSignalSpy modelResetSpy(&model, &QAbstractItemModel::modelReset);
model.updateModel(makeAccountInfo(), makePremiumServerConfig());
QCOMPARE(model.rowCount(), 1);
QCOMPARE(modelResetSpy.count(), 1);
const QModelIndex index = model.index(0, 0);
QVERIFY(index.isValid());
QCOMPARE(model.data(index, ApiAccountInfoModel::ConnectedDevicesRole).toString(), QStringLiteral("2 out of 5"));
QCOMPARE(model.data(index, ApiAccountInfoModel::ServiceDescriptionRole).toString(), QStringLiteral("Premium subscription"));
QCOMPARE(model.data(index, ApiAccountInfoModel::IsSubscriptionRenewalAvailableRole).toBool(), true);
QCOMPARE(model.data(index, ApiAccountInfoModel::IsInAppPurchaseRole).toBool(), false);
QCOMPARE(model.data(index, ApiAccountInfoModel::IsComponentVisibleRole).toBool(), true);
QCOMPARE(model.data(QStringLiteral("serviceDescription")).toString(), QStringLiteral("Premium subscription"));
}
void testSupportLinks()
{
ApiAccountInfoModel model;
model.updateModel(makeAccountInfo(), makePremiumServerConfig());
QCOMPARE(model.getTelegramBotLink(), QStringLiteral("https://t.me/test_support"));
QCOMPARE(model.getEmailLink(), QStringLiteral("support@example.org"));
QCOMPARE(model.getBillingEmailLink(), QStringLiteral("billing@example.org"));
QCOMPARE(model.getSiteLink(), QStringLiteral("Example"));
QCOMPARE(model.getFullSiteLink(), QStringLiteral("https://example.org"));
}
void testAvailableCountriesAndIssuedConfigs()
{
ApiAccountInfoModel model;
model.updateModel(makeAccountInfo(), makePremiumServerConfig());
QCOMPARE(model.getAvailableCountries(), QJsonArray({ "ru", "de" }));
QCOMPARE(model.getIssuedConfigsInfo(), QJsonArray { });
}
void testMissingOptionalFields()
{
ApiAccountInfoModel model;
model.updateModel(QJsonObject { }, makePremiumServerConfig());
const QModelIndex index = model.index(0, 0);
QVERIFY(index.isValid());
QCOMPARE(model.data(index, ApiAccountInfoModel::ConnectedDevicesRole).toString(), QStringLiteral("0 out of 0"));
QCOMPARE(model.data(index, ApiAccountInfoModel::ServiceDescriptionRole).toString(), QString());
QCOMPARE(model.data(index, ApiAccountInfoModel::IsSubscriptionRenewalAvailableRole).toBool(), false);
QCOMPARE(model.data(index, ApiAccountInfoModel::IsInAppPurchaseRole).toBool(), false);
QVERIFY(model.getTelegramBotLink().isEmpty());
QVERIFY(model.getEmailLink().isEmpty());
QVERIFY(model.getBillingEmailLink().isEmpty());
QVERIFY(model.getSiteLink().isEmpty());
QVERIFY(model.getFullSiteLink().isEmpty());
}
void testUpdateModelResetsData()
{
ApiAccountInfoModel model;
model.updateModel(makeAccountInfo(), makePremiumServerConfig());
QCOMPARE(model.data(model.index(0, 0), ApiAccountInfoModel::ConnectedDevicesRole).toString(), QStringLiteral("2 out of 5"));
QJsonObject updatedInfo = makeAccountInfo();
updatedInfo[apiDefs::key::activeDeviceCount] = 1;
updatedInfo[apiDefs::key::maxDeviceCount] = 3;
updatedInfo[apiDefs::key::subscriptionDescription] = "Updated subscription";
model.updateModel(updatedInfo, makePremiumServerConfig());
QCOMPARE(model.data(model.index(0, 0), ApiAccountInfoModel::ConnectedDevicesRole).toString(), QStringLiteral("1 out of 3"));
QCOMPARE(model.data(model.index(0, 0), ApiAccountInfoModel::ServiceDescriptionRole).toString(), QStringLiteral("Updated subscription"));
}
};
QTEST_MAIN(TestUiApiAccountInfoModel)
#include "testUiApiAccountInfoModel.moc"
@@ -1,97 +0,0 @@
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QTest>
#include <QUuid>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
class TestUiApiServicesModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
// TODO: add env vars for api
private slots:
void initTestCase()
{
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase()
{
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init()
{
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testRolesAndSignals()
{
QSignalSpy errorOccurredSpy(m_coreController->m_servicesCatalogUiController, &ServicesCatalogUiController::errorOccurred);
/* TODO:
m_coreController->m_servicesCatalogUiController->fillAvailableServices();
QVERIFY(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QModelIndex serviceModelIndex = m_coreController->m_apiServicesModel->index(0, 0);
QVERIFY2(serviceModelIndex.isValid(), "Service model index should be valid");
auto serviceName = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::NameRole);
QCOMPARE(serviceName, );
auto serviceCardDescription = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::CardDescriptionRole);
QCOMPARE(serviceCardDescription, );
auto isServiceAvailable = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::IsServiceAvailableRole);
QCOMPARE(isServiceAvailable, );
auto serviceSpeed = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::SpeedRole);
QCOMPARE(serviceSpeed, );
auto serviceTimeLimit = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::TimeLimitRole);
QCOMPARE(serviceTimeLimit, );
auto serviceRegion = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::RegionRole);
QCOMPARE(serviceRegion, );
auto serviceFeatures = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::FeaturesRole);
QCOMPARE(serviceFeatures, );
auto servicePrice = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::PriceRole);
QCOMPARE(servicePrice, );
auto serviceEndDate = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::EndDateRole);
QCOMPARE(serviceEndDate, );
auto serviceOrder = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::OrderRole);
QCOMPARE(serviceOrder, );
*/
}
};
QTEST_MAIN(TestUiApiServicesModelAndController)
#include "testUiApiServicesModelAndController.moc"
+236 -33
View File
@@ -1,17 +1,92 @@
#include <QDateTime>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QTest>
#include <QUuid>
#include <QProcessEnvironment>
#include "utils/testCoreController.h"
#include "utils/testUtils.h"
#include "core/models/serverDescription.h"
#include "core/controllers/api/subscriptionController.h"
#include "core/models/api/apiV2ServerConfig.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/serverConfigUtils.h"
#include "ui/controllers/api/apiNewsUiController.h"
#include "ui/models/newsModel.h"
#include "amneziaApplication.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
using namespace amnezia::test;
namespace
{
QJsonObject gatewayServerConfig()
{
QJsonObject lastConfig;
lastConfig[configKey::hostName] = QStringLiteral("10.0.0.1");
lastConfig[configKey::clientPrivKey] = QStringLiteral("test-private-key");
lastConfig[configKey::persistentKeepAlive] = QStringLiteral("25-35");
QJsonObject awgConfig;
awgConfig[configKey::lastConfig] = QString(QJsonDocument(lastConfig).toJson(QJsonDocument::Compact));
awgConfig[configKey::port] = QStringLiteral("35333");
QJsonObject awgContainer;
awgContainer[configKey::container] = QStringLiteral("amnezia-awg");
awgContainer[QString(configKey::awg)] = awgConfig;
QJsonObject config;
config[configKey::name] = QStringLiteral("Amnezia Premium");
config[configKey::description] = QStringLiteral("Premium service");
config[configKey::hostName] = QStringLiteral("gateway.example.org");
config[configKey::configVersion] = serverConfigUtils::ConfigSource::AmneziaGateway;
// config[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
config[configKey::defaultContainer] = QStringLiteral("amnezia-awg");
config[configKey::containers] = QJsonArray { awgContainer };
return config;
}
ApiV2ServerConfig gatewayTestSubscription(const QString &apiKey)
{
ApiV2ServerConfig config = ApiV2ServerConfig::fromJson(gatewayServerConfig());
config.apiConfig.serviceType = QStringLiteral("amnezia-premium");
config.apiConfig.serviceProtocol = QString(configKey::awg);
config.apiConfig.userCountryCode = QStringLiteral("ru");
config.apiConfig.vpnKey = QStringLiteral("vpn://stored-key");
config.authData.apiKey = apiKey;
config.crc = 42;
return config;
}
QString envValue(const char *name)
{
return QProcessEnvironment::systemEnvironment().value(QString::fromLatin1(name)).trimmed();
}
QJsonObject makeNewsItem(const QString &id, const QString &title, const QString &content, const QString &timestamp)
{
return { { "id", id }, { "title", title }, { "content", content }, { "timestamp", timestamp } };
}
QJsonArray makeNews()
{
return { makeNewsItem("news-1", "First news", "First content", "2026-09-29T10:00:00Z"),
makeNewsItem("news-2", "Second news", "Second content", "2026-09-28T10:00:00Z"),
makeNewsItem("news-3", "Third news", "Third content", "2026-09-27T10:00:00Z") };
}
} // namespace
class TestUiNewsModelAndController : public QObject
{
@@ -21,15 +96,13 @@ private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
// TODO: add env vars for api
private slots:
void initTestCase()
{
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
const QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
const auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
@@ -44,54 +117,184 @@ private slots:
void init()
{
m_settings->clearSettings();
m_coreController->m_serversRepository->clearServers();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
m_coreController->m_newsModel->setNewsList({});
}
void testRolesAndSignals()
{
NewsModel *model = m_coreController->m_newsModel;
model->setNewsList(makeNews());
QSignalSpy hasUnreadChangedSpy(model, &NewsModel::hasUnreadChanged);
QSignalSpy processedIndexChangedSpy(model, &NewsModel::processedIndexChanged);
QCOMPARE(model->rowCount(), 3);
QVERIFY(model->hasUnread());
const QModelIndex index = model->index(0, 0);
QVERIFY2(index.isValid(), "News model index should be valid");
QCOMPARE(model->data(index, NewsModel::IdRole).toString(), QString("news-1"));
QCOMPARE(model->data(index, NewsModel::TitleRole).toString(), QString("First news"));
QCOMPARE(model->data(index, NewsModel::ContentRole).toString(), QString("First content"));
const QDateTime timestamp = QDateTime::fromString(model->data(index, NewsModel::TimestampRole).toString(), Qt::ISODate);
QVERIFY(timestamp.isValid());
const QDateTime expectedTimestamp = QDateTime::fromString("2026-09-29T10:00:00Z", Qt::ISODate).toLocalTime();
QCOMPARE(timestamp, expectedTimestamp);
QCOMPARE(model->data(index, NewsModel::IsReadRole).toBool(), false);
QCOMPARE(model->data(index, NewsModel::IsProcessedRole).toBool(), false);
model->setProcessedIndex(0);
QCOMPARE(model->processedIndex(), 0);
QCOMPARE(processedIndexChangedSpy.count(), 1);
QCOMPARE(model->data(index, NewsModel::IsProcessedRole).toBool(), true);
model->markAsRead(0);
QVERIFY(model->data(index, NewsModel::IsReadRole).toBool());
QCOMPARE(hasUnreadChangedSpy.count(), 1);
QVERIFY(model->hasUnread());
model->markAsRead(0);
QCOMPARE(hasUnreadChangedSpy.count(), 1);
QVERIFY(model->data(index, NewsModel::IsReadRole).toBool());
model->markAsRead(-1);
model->markAsRead(model->rowCount());
QCOMPARE(hasUnreadChangedSpy.count(), 1);
QVERIFY(model->hasUnread());
}
void testSorting()
{
NewsModel *model = m_coreController->m_newsModel;
model->setNewsList({ makeNewsItem("old", "Old", "Old content", "2026-09-27T10:00:00Z"),
makeNewsItem("new", "New", "New content", "2026-09-29T10:00:00Z"),
makeNewsItem("middle", "Middle", "Middle content", "2026-09-28T10:00:00Z") });
QCOMPARE(model->rowCount(), 3);
QCOMPARE(model->data(model->index(0, 0), NewsModel::IdRole).toString(), QString("new"));
QCOMPARE(model->data(model->index(1, 0), NewsModel::IdRole).toString(), QString("middle"));
QCOMPARE(model->data(model->index(2, 0), NewsModel::IdRole).toString(), QString("old"));
}
void testInvalidItemsAreIgnored()
{
NewsModel *model = m_coreController->m_newsModel;
model->setNewsList(
{ QJsonValue("invalid"),
QJsonObject { { "title", "No id" }, { "content", "Content" }, { "timestamp", "2026-09-29T10:00:00Z" } },
makeNewsItem("valid", "Valid", "Valid content", "2026-09-29T10:00:00Z") });
QCOMPARE(model->rowCount(), 1);
QCOMPARE(model->data(model->index(0, 0), NewsModel::IdRole).toString(), QString("valid"));
}
void testReadStatePersistence()
{
const QJsonArray news { makeNewsItem("persist-1", "First", "Content 1", "2026-09-29T10:00:00Z"),
makeNewsItem("persist-2", "Second", "Content 2", "2026-09-28T10:00:00Z") };
NewsModel *model = m_coreController->m_newsModel;
model->setNewsList(news);
model->markAsRead(0);
const QStringList savedIds = m_coreController->m_appSettingsRepository->getReadNewsIds();
QVERIFY(savedIds.contains(QStringLiteral("persist-1")));
NewsModel restoredModel(m_coreController->m_appSettingsRepository);
restoredModel.setNewsList(news);
QCOMPARE(restoredModel.data(restoredModel.index(0, 0), NewsModel::IsReadRole).toBool(), true);
QCOMPARE(restoredModel.data(restoredModel.index(1, 0), NewsModel::IsReadRole).toBool(), false);
}
void testFetchNewsWithoutServers()
{
QSignalSpy fetchNewsFinishedSpy(m_coreController->m_apiNewsUiController, &ApiNewsUiController::fetchNewsFinished);
QSignalSpy errorOccurredSpy(m_coreController->m_apiNewsUiController, &ApiNewsUiController::errorOccurred);
QSignalSpy processedIndexChangedSpy(m_coreController->m_newsModel, &NewsModel::processedIndexChanged);
QSignalSpy hasUnreadChangedSpy(m_coreController->m_newsModel, &NewsModel::hasUnreadChanged);
/* TODO:
m_coreController->m_apiNewsUiController->fetchNews(false);
QVERIFY(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QVERIFY(fetchNewsFinishedSpy.count() == 1, "fetchNewsFinished signal should be emitted");
m_coreController->m_newsModel->updateModel();
QVERIFY(hasUnreadChangedSpy.count() == 1, "hasUnreadChanged signal should be emitted");
QTRY_COMPARE_WITH_TIMEOUT(fetchNewsFinishedSpy.count(), 1, 1000);
QCOMPARE(errorOccurredSpy.count(), 0);
QCOMPARE(m_coreController->m_newsModel->rowCount(), 0);
}
QModelIndex newsModelIndex = m_coreController->m_newsModel->index(0, 0);
QVERIFY2(newsModelIndex.isValid(), "News model index should be valid");
void testFetchNewsFromApi()
{
const QString apiKey = envValue("AMNEZIA_TEST_API_KEY");
auto newsId = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::IdRole);
QCOMPARE(newsId, );
if (apiKey.isEmpty()) {
QSKIP("AMNEZIA_TEST_API_KEY is not set, skipping live gateway test");
}
auto newsTitle = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::TitleRole);
QCOMPARE(newsTitle, );
m_settings->setValue("Conf/installationUuid", QUuid::createUuidV5(QUuid(), QByteArrayLiteral("amnezia-news-api-test")).toString(QUuid::WithoutBraces));
auto newsContent = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::ContentRole);
QCOMPARE(newsContent, );
const ApiV2ServerConfig seed = gatewayTestSubscription(apiKey);
const QString serverId = m_coreController->m_serversRepository->addServer(QString(), seed.toJson(), serverConfigUtils::configTypeFromJson(seed.toJson()));
auto newsTimestamp = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::TimestampRole);
QCOMPARE(newsTimestamp, );
QCOMPARE(m_coreController->m_serversRepository->serversCount(), 1);
auto newsIsRead = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::IsReadRole);
QCOMPARE(newsIsRead, false);
SubscriptionController::CaptchaInfo captchaInfo;
auto newsIsProcessed = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::IsProcessedRole);
QCOMPARE(newsIsProcessed, );
const ErrorCode updateError = m_coreController->m_subscriptionController->updateServiceFromGateway(serverId, QStringLiteral("de"), /*isConnectEvent=*/false, &captchaInfo, nullptr);
m_coreController->m_newsModel->markAsRead(0);
? m_coreController->m_newsModel->updateModel(); ?
QVERIFY(hasUnreadChangedSpy.count() == 2, "hasUnreadChanged signal should be emitted");
QCOMPARE(newsIsRead, true);
*/
if (updateError == ErrorCode::ApiCaptchaRequiredError) {
QSKIP("Gateway requested a captcha, skipping live gateway test");
}
QVERIFY2(updateError == ErrorCode::NoError, qPrintable(QStringLiteral("Gateway update failed: %1").arg(static_cast<int>(updateError))));
QCOMPARE(m_coreController->m_serversRepository->serversCount(), 1);
QSignalSpy fetchNewsFinishedSpy(m_coreController->m_apiNewsUiController, &ApiNewsUiController::fetchNewsFinished);
QSignalSpy errorOccurredSpy(m_coreController->m_apiNewsUiController, &ApiNewsUiController::errorOccurred);
m_coreController->m_apiNewsUiController->fetchNews(false);
QTRY_COMPARE_WITH_TIMEOUT(fetchNewsFinishedSpy.count(), 1, 10000);
QCOMPARE(errorOccurredSpy.count(), 0);
QVERIFY(m_coreController->m_newsModel->rowCount() > 0);
const QModelIndex index = m_coreController->m_newsModel->index(0, 0);
QVERIFY(index.isValid());
QVERIFY(!m_coreController->m_newsModel->data(index, NewsModel::IdRole).toString().isEmpty());
QVERIFY(!m_coreController->m_newsModel->data(index, NewsModel::TitleRole).toString().isEmpty());
QVERIFY(!m_coreController->m_newsModel->data(index, NewsModel::ContentRole).toString().isEmpty());
const QDateTime timestamp = QDateTime::fromString(m_coreController->m_newsModel->data(index, NewsModel::TimestampRole).toString(), Qt::ISODate);
QVERIFY(timestamp.isValid());
QCOMPARE(m_coreController->m_newsModel->data(index, NewsModel::IsReadRole).toBool(), false);
}
};
QTEST_MAIN(TestUiNewsModelAndController)
int main(int argc, char *argv[])
{
// CoreController accesses the gateway through amnApp->networkManager().
// The application instance must be an AmneziaApplication.
AmneziaApplication app(argc, argv);
TestUiNewsModelAndController tc;
QTEST_SET_MAIN_SOURCE_PATH
return QTest::qExec(&tc, argc, argv);
}
#include "testUiNewsModelAndController.moc"
@@ -0,0 +1,310 @@
#include <QDateTime>
#include <QJsonArray>
#include <QJsonObject>
#include <QSignalSpy>
#include <QTest>
#include <QUuid>
#include "utils/testCoreController.h"
#include "utils/testUtils.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
#include "ui/models/api/apiServicesModel.h"
using namespace amnezia;
using namespace amnezia::test;
namespace
{
QJsonObject makeServiceItem(const QString &name,
const QString &cardDescription,
const QString &description,
const QString &type,
const QString &protocol,
const bool isAvailable,
const QString &price,
const QString &endDate,
const QString &termsOfUseUrl,
const QString &privacyPolicyUrl,
const QJsonArray &subscriptionPlans = {})
{
return {
{ "service_info",
QJsonObject {
{ "id", type + "-info" },
{ "name", name }
}
},
{ "service_type", type },
{ "service_protocol", protocol },
{ "is_available", isAvailable },
{ "service_description",
QJsonObject {
{ "service_name", name },
{ "card_description", cardDescription },
{ "description", description },
{ "terms_of_use_url", termsOfUseUrl },
{ "privacy_policy_url", privacyPolicyUrl },
{ "subscription_plans", subscriptionPlans },
{ "min_price_label", price }
}
},
{ "subscription",
QJsonObject {
{ "end_date", endDate }
}
},
{ "available_countries",
QJsonArray {
"EE",
"FI"
}
},
{ "store_endpoint", "https://example.com/store" }
};
}
QJsonObject makeServicesResponse()
{
return {
{ "user_country_code", "EE" },
{ "services",
QJsonArray {
makeServiceItem("Amnezia Premium",
"Premium service",
"Premium service description",
"amnezia-premium",
"xray",
true,
"$19.99",
"2026-10-29T10:00:00Z",
"https://example.com/terms",
"https://example.com/privacy",
QJsonArray {
QJsonObject {
{ "id", "monthly" },
{ "price", "$19.99" }
}
}),
makeServiceItem("Amnezia Free",
"Free service",
"Free service description",
"amnezia-free",
"wireguard",
true,
"$0",
"",
"https://example.com/terms",
"https://example.com/privacy")
}
}
};
}
} // namespace
class TestUiServicesModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
private slots:
void initTestCase()
{
const QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
const auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase()
{
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init()
{
m_settings->clearSettings();
m_coreController->m_apiServicesModel->updateModel(QJsonObject {});
}
void testRolesAndSignals()
{
ApiServicesModel *model = m_coreController->m_apiServicesModel;
QSignalSpy selectionChangedSpy(model, &ApiServicesModel::serviceSelectionChanged);
model->updateModel(makeServicesResponse());
QCOMPARE(model->rowCount(), 2);
QCOMPARE(selectionChangedSpy.count(), 1);
const QModelIndex firstIndex = model->index(0, 0);
QCOMPARE(model->data(firstIndex, ApiServicesModel::NameRole).toString(), QString("Amnezia Premium"));
QCOMPARE(model->data(firstIndex, ApiServicesModel::CardDescriptionRole).toString(), QString("Premium service"));
QCOMPARE(model->data(firstIndex, ApiServicesModel::ServiceDescriptionRole).toString(), QString("Premium service description"));
QCOMPARE(model->data(firstIndex, ApiServicesModel::IsServiceAvailableRole).toBool(), true);
QCOMPARE(model->data(firstIndex, ApiServicesModel::IsPremiumRole).toBool(), true);
QCOMPARE(model->data(firstIndex, ApiServicesModel::HasSubscriptionPlansRole).toBool(), true);
QCOMPARE(model->data(firstIndex, ApiServicesModel::PriceRole).toString(), QString("$19.99"));
QCOMPARE(model->data(firstIndex, ApiServicesModel::TermsOfUseUrlRole).toString(),QString("https://example.com/terms"));
QCOMPARE(model->data(firstIndex, ApiServicesModel::PrivacyPolicyUrlRole).toString(), QString("https://example.com/privacy"));
QCOMPARE(model->data(firstIndex, ApiServicesModel::ShowRecommendedRole).toBool(), true);
QCOMPARE(model->data(firstIndex, ApiServicesModel::OrderRole).toInt(), 0);
}
void testServiceSelection()
{
ApiServicesModel *model = m_coreController->m_apiServicesModel;
model->updateModel(makeServicesResponse());
QCOMPARE(model->serviceIndexForType("amnezia-premium"), 0);
QCOMPARE(model->serviceIndexForType("amnezia-free"), 1);
QCOMPARE(model->serviceIndexForType("unknown"), -1);
QSignalSpy selectionChangedSpy(model, &ApiServicesModel::serviceSelectionChanged);
const QJsonArray expectedCountries { "EE", "FI" };
model->setServiceIndex(1);
QCOMPARE(selectionChangedSpy.count(), 1);
QCOMPARE(model->getSelectedServiceName(), QString("Amnezia Free"));
QCOMPARE(model->getSelectedServiceType(), QString("amnezia-free"));
QCOMPARE(model->getSelectedServiceProtocol(), QString("wireguard"));
QCOMPARE(model->getSelectedServiceCountries(), expectedCountries);
QCOMPARE(model->getCountryCode(), QString("EE"));
QCOMPARE(model->getStoreEndpoint(), QString("https://example.com/store"));
}
void testSelectedServiceData()
{
ApiServicesModel *model = m_coreController->m_apiServicesModel;
model->updateModel(makeServicesResponse());
QCOMPARE(model->getSelectedServiceData("name").toString(), QString("Amnezia Premium"));
QCOMPARE(model->getSelectedServiceData("cardDescription").toString(), QString("Premium service"));
QCOMPARE(model->getSelectedServiceData("serviceDescription").toString(), QString("Premium service description"));
QCOMPARE(model->getSelectedServiceData("isServiceAvailable").toBool(), true);
QCOMPARE(model->getSelectedServiceData("isPremium").toBool(), true);
QCOMPARE(model->getSelectedServiceData("hasSubscriptionPlans").toBool(), true);
QCOMPARE(model->getSelectedServiceData("price").toString(), QString("$19.99"));
QCOMPARE(model->getSelectedServiceData("termsOfUseUrl").toString(), QString("https://example.com/terms"));
QCOMPARE(model->getSelectedServiceData("privacyPolicyUrl").toString(), QString("https://example.com/privacy"));
QVERIFY(!model->getSelectedServiceData("unknownRole").isValid());
}
void testEndDate()
{
ApiServicesModel *model = m_coreController->m_apiServicesModel;
model->updateModel(makeServicesResponse());
const QString endDate = model->data(model->index(0, 0), ApiServicesModel::EndDateRole).toString();
const QString expected = QDateTime::fromString("2026-10-29T10:00:00Z", Qt::ISODate).toLocalTime().toString("d MMM yyyy");
QCOMPARE(endDate, expected);
}
void testFreeServiceUnavailable()
{
ApiServicesModel *model = m_coreController->m_apiServicesModel;
QJsonArray services { makeServiceItem("Amnezia Free", "Free service", "Free service description", "amnezia-free",
"wireguard", false, "$0", "", "https://example.com/terms", "https://example.com/privacy") };
QJsonObject data { { "services", services } };
model->updateModel(data);
const QModelIndex index = model->index(0, 0);
QCOMPARE(model->data(index, ApiServicesModel::IsServiceAvailableRole).toBool(), false);
QCOMPARE(model->data(index, ApiServicesModel::IsPremiumRole).toBool(), false);
QCOMPARE(model->data(index, ApiServicesModel::ShowRecommendedRole).toBool(), false);
QCOMPARE(model->data(index, ApiServicesModel::OrderRole).toInt(), 1);
const QString description = model->data(index, ApiServicesModel::CardDescriptionRole).toString();
QVERIFY(description.contains("Not available in your region"));
}
void testInvalidModelIndex()
{
ApiServicesModel *model = m_coreController->m_apiServicesModel;
model->updateModel(makeServicesResponse());
QVERIFY(!model->data(QModelIndex(), ApiServicesModel::NameRole).isValid());
QVERIFY(!model->data(model->index(-1, 0), ApiServicesModel::NameRole).isValid());
QVERIFY(!model->data(model->index(model->rowCount(), 0), ApiServicesModel::NameRole).isValid());
}
void testEmptyResponse()
{
ApiServicesModel *model = m_coreController->m_apiServicesModel;
QSignalSpy selectionChangedSpy(model, &ApiServicesModel::serviceSelectionChanged);
model->updateModel(QJsonObject {
{ "user_country_code", "EE" },
{ "services", QJsonArray {} }
});
QCOMPARE(model->rowCount(), 1);
QCOMPARE(selectionChangedSpy.count(), 1);
}
void testServicesWithoutSubscriptionPlans()
{
ApiServicesModel *model = m_coreController->m_apiServicesModel;
model->updateModel(makeServicesResponse());
const QModelIndex index = model->index(1, 0);
QCOMPARE(model->data(index, ApiServicesModel::HasSubscriptionPlansRole).toBool(), false);
}
void testControllerAccessors()
{
ApiServicesModel *model = m_coreController->m_apiServicesModel;
model->updateModel(makeServicesResponse());
model->setServiceIndex(0);
ServicesCatalogUiController *controller = m_coreController->m_servicesCatalogUiController;
const QJsonObject expectedServiceInfo { { "id", "amnezia-premium-info" }, { "name", "Amnezia Premium" } };
QCOMPARE(controller->getSelectedServiceInfo(), expectedServiceInfo);
QCOMPARE(controller->getSelectedServiceType(), model->getSelectedServiceType());
QCOMPARE(controller->getSelectedServiceProtocol(), model->getSelectedServiceProtocol());
QCOMPARE(controller->getSelectedServiceName(), model->getSelectedServiceName());
QCOMPARE(controller->getSelectedServiceCountries(), model->getSelectedServiceCountries());
QCOMPARE(controller->getCountryCode(), model->getCountryCode());
QCOMPARE(controller->getStoreEndpoint(), model->getStoreEndpoint());
QCOMPARE(controller->getSelectedServiceData("name"), model->getSelectedServiceData("name"));
QCOMPARE(controller->getSelectedServiceData("isPremium"), model->getSelectedServiceData("isPremium"));
}
void testControllerErrorSignal()
{
ServicesCatalogUiController *controller = m_coreController->m_servicesCatalogUiController;
QSignalSpy errorSpy(controller, &ServicesCatalogUiController::errorOccurred);
QVERIFY(errorSpy.isValid());
}
};
QTEST_MAIN(TestUiServicesModelAndController)
#include "testUiServicesModelAndController.moc"