mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-10-25 12:36:46 +09:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c226265e33 | ||
|
|
35b65484e7 | ||
|
|
c60b04ca46 | ||
|
|
051f4ca6ef | ||
|
|
69d87e70ac | ||
|
|
b7ea6c498d | ||
|
|
8ad00d533e | ||
|
|
b4c5c381ba | ||
|
|
3c25dfce55 | ||
|
|
d8fa1f5161 | ||
|
|
6960287ef5 | ||
|
|
1386336f10 | ||
|
|
c482fa51a7 | ||
|
|
f49fa1ac60 | ||
|
|
34101a91d9 | ||
|
|
972b4e1274 | ||
|
|
df47fdcae0 | ||
|
|
bc5ae6e410 | ||
|
|
0d38b22162 | ||
|
|
4773fb0821 | ||
|
|
a91e1a20f0 | ||
|
|
24b74fcb3e | ||
|
|
ed3deef2dd | ||
|
|
185c85e183 | ||
|
|
bb7cf69cfc | ||
|
|
5ba704ffcf | ||
|
|
1b47d79f26 | ||
|
|
a5cb5679ae | ||
|
|
6aefc6be47 | ||
|
|
378b30f553 | ||
|
|
812ee905f4 | ||
|
|
b16d603435 | ||
|
|
c107c01cab | ||
|
|
80ff2e0676 | ||
|
|
4b6f09d55f | ||
|
|
c6958d8b70 |
No files matched your search
@@ -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
|
||||
@@ -1508,98 +1535,16 @@ jobs:
|
||||
# KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }}
|
||||
# run: echo $KEYSTORE_BASE64 | base64 --decode > android.keystore
|
||||
|
||||
- name: 'Configure (Android x86_64)'
|
||||
env:
|
||||
QT_ANDROID: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/android_x86_64
|
||||
QT_HOST: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64
|
||||
CMAKE_ANDROID_NDK: ${{ env.ANDROID_HOME }}/ndk/${{ env.NDK_VERSION }}
|
||||
run: |
|
||||
cmake -S . -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_TOOLCHAIN_FILE=$QT_ANDROID/lib/cmake/Qt6/qt.toolchain.cmake \
|
||||
-DANDROID_PLATFORM=${{ env.ANDROID_PLATFORM }} \
|
||||
-DCMAKE_ANDROID_NDK=$CMAKE_ANDROID_NDK \
|
||||
-DQT_HOST_PATH=$QT_HOST \
|
||||
-DANDROID_SDK_ROOT=$ANDROID_HOME \
|
||||
-DAMNEZIA_BUILD_TESTS=ON
|
||||
|
||||
- name: 'Build Android tests'
|
||||
# env:
|
||||
# QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
|
||||
# QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
|
||||
# QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
|
||||
run: |
|
||||
cmake --build build --target test_android_openssl_make_apk
|
||||
cmake --build build --target test_android_restore_backup_make_apk
|
||||
|
||||
- name: 'Write Android test runner script'
|
||||
run: |
|
||||
cat > ci_run_android_tests.sh << 'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ADB=$(which adb)
|
||||
|
||||
echo ">>> Waiting for package manager to be ready..."
|
||||
"$ADB" shell 'while ! pm list packages > /dev/null 2>&1; do sleep 2; done'
|
||||
echo ">>> Package manager ready."
|
||||
|
||||
run_test() {
|
||||
local target="$1"
|
||||
local apk="build/client/tests/android-build/${target}.apk"
|
||||
local pkg_name="org.qtproject.example.${target}"
|
||||
local activity="${pkg_name}/org.qtproject.qt.android.bindings.QtActivity"
|
||||
|
||||
echo ">>> Installing ${target} APK..."
|
||||
timeout 120 "$ADB" install -r "$apk"
|
||||
|
||||
"$ADB" shell "run-as $pkg_name sh -c 'mkdir -p files && : > files/stdout.txt'" 2>/dev/null || true
|
||||
"$ADB" shell "run-as $pkg_name sh -c 'rm -f files/qtest_last_exit_code'" 2>/dev/null || true
|
||||
|
||||
echo ">>> Launching ${target}..."
|
||||
"$ADB" shell am start -n "$activity"
|
||||
|
||||
echo ">>> Waiting for test to finish (up to 120s)..."
|
||||
local elapsed=0 exit_code=""
|
||||
while [[ $elapsed -lt 120 ]]; do
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
exit_code=$("$ADB" shell \
|
||||
"run-as $pkg_name cat files/qtest_last_exit_code 2>/dev/null" \
|
||||
| tr -d '[:space:]' || true)
|
||||
[[ -n "$exit_code" ]] && break
|
||||
done
|
||||
|
||||
local stdout_content
|
||||
stdout_content=$("$ADB" shell \
|
||||
"run-as $pkg_name cat files/stdout.txt 2>/dev/null" || true)
|
||||
if [[ -n "$stdout_content" ]]; then
|
||||
echo ">>> Test output:"
|
||||
echo "$stdout_content"
|
||||
fi
|
||||
|
||||
if [[ -z "$exit_code" ]]; then
|
||||
echo ">>> TIMEOUT: ${target} did not complete within 120s."
|
||||
exit 1
|
||||
elif [[ "$exit_code" != "0" ]]; then
|
||||
echo ">>> ${target} FAILED (exit code: $exit_code)"
|
||||
exit 1
|
||||
else
|
||||
echo ">>> ${target} PASSED"
|
||||
fi
|
||||
}
|
||||
|
||||
run_test test_android_openssl
|
||||
run_test test_android_restore_backup
|
||||
EOF
|
||||
chmod +x ci_run_android_tests.sh
|
||||
|
||||
- name: 'Run Android tests on emulator'
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
env:
|
||||
QT_ROOT: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}
|
||||
NDK_VERSION: ${{ env.NDK_VERSION }}
|
||||
ANDROID_PLATFORM: ${{ env.ANDROID_PLATFORM }}
|
||||
with:
|
||||
api-level: 28
|
||||
arch: x86_64
|
||||
emulator-options: -no-snapshot -no-window -no-audio -no-boot-anim -accel on
|
||||
disable-animations: true
|
||||
script: bash ci_run_android_tests.sh
|
||||
script: bash client/tests/run_android_tests.sh --wait-boot
|
||||
|
||||
@@ -416,8 +416,8 @@ endif()
|
||||
|
||||
if(NOT ANDROID)
|
||||
add_subdirectory(selfHostedAdmin)
|
||||
add_subdirectory(api)
|
||||
endif()
|
||||
add_subdirectory(api)
|
||||
|
||||
enable_testing()
|
||||
|
||||
@@ -443,7 +443,7 @@ if(NOT ANDROID)
|
||||
add_test(NAME LogsControllerTest COMMAND test_logs_controller)
|
||||
endif()
|
||||
|
||||
# Android tests are executed on-device via androidtestrunner in CI (see .github/workflows/tests.yml).
|
||||
# Android tests are executed on-device via androidtestrunner in CI (see .github/workflows/deploy.yml).
|
||||
# CTest cannot run Android binaries on the host machine, so no add_test() entries are registered here.
|
||||
|
||||
amnezia_copy_runtime_dlls()
|
||||
@@ -1,32 +1,77 @@
|
||||
add_executable(test_ui_api_services_model_and_controller
|
||||
testUiApiServicesModelAndController.cpp
|
||||
)
|
||||
function(add_new_test target source)
|
||||
if(ANDROID)
|
||||
qt_add_executable(${target}
|
||||
${source}
|
||||
${ARGN}
|
||||
)
|
||||
else()
|
||||
add_executable(${target}
|
||||
${source}
|
||||
${ARGN}
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(test_ui_api_services_model_and_controller PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_ui_news_model_and_controller
|
||||
# These tests are selected for execution on the Android emulator.
|
||||
if(ANDROID AND "${target}" MATCHES
|
||||
"^(test_ui_api_account_info_model|test_ui_subscription_controller|test_ui_services_model_and_controller)$")
|
||||
set_target_properties(${target} PROPERTIES
|
||||
QT_ANDROID_PACKAGE_SOURCE_DIR "${ANDROID_TEST_PACKAGE_DIR}"
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
add_new_test(test_ui_news_model_and_controller
|
||||
testUiNewsModelAndController.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_ui_news_model_and_controller PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_gateway_service_import
|
||||
add_new_test(test_gateway_service_import
|
||||
testGatewayServiceImport.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_gateway_service_import PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
add_new_test(test_ui_services_model_and_controller
|
||||
testUiServicesModelAndController.cpp
|
||||
)
|
||||
|
||||
add_test(NAME UiApiServicesModelAndControllerTest COMMAND test_ui_api_services_model_and_controller)
|
||||
add_new_test(test_ui_subscription_controller
|
||||
testSubscriptionUiController.cpp
|
||||
)
|
||||
|
||||
add_new_test(test_ui_api_account_info_model
|
||||
testUiApiAccountInfoModel.cpp
|
||||
)
|
||||
|
||||
add_new_test(test_api_benefits_model
|
||||
testApiBenefitsModel.cpp
|
||||
)
|
||||
|
||||
add_new_test(test_api_country_model
|
||||
testApiCountryModel.cpp
|
||||
)
|
||||
|
||||
add_new_test(test_api_country_list_model
|
||||
testApiCountryListModel.cpp
|
||||
${CLIENT_ROOT_DIR}/resources/countries.qrc
|
||||
)
|
||||
|
||||
add_new_test(test_api_devices_model
|
||||
testApiDevicesModel.cpp
|
||||
)
|
||||
|
||||
if(NOT ANDROID)
|
||||
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)
|
||||
endif()
|
||||
|
||||
amnezia_copy_runtime_dlls()
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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 ×tamp)
|
||||
{
|
||||
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"
|
||||
@@ -19,6 +19,10 @@
|
||||
# bash client/tests/run_android_tests.sh --abi arm64-v8a # physical ARM device
|
||||
# bash client/tests/run_android_tests.sh --test openssl # only openssl test
|
||||
# bash client/tests/run_android_tests.sh --test restore_backup # only restore-backup test
|
||||
# bash client/tests/run_android_tests.sh --test api # selected API tests
|
||||
# bash client/tests/run_android_tests.sh --test account_info # account info model test
|
||||
# bash client/tests/run_android_tests.sh --test subscription_controller
|
||||
# bash client/tests/run_android_tests.sh --test services_model_and_controller
|
||||
# bash client/tests/run_android_tests.sh --wait-boot # wait for emulator to boot first
|
||||
|
||||
set -euo pipefail
|
||||
@@ -33,7 +37,7 @@ ANDROID_PLATFORM="${ANDROID_PLATFORM:-android-28}"
|
||||
ABI="${ABI:-x86_64}"
|
||||
ABI_DIR="${ABI//-/_}"
|
||||
BUILD_DIR="${BUILD_DIR:-build/android_${ABI_DIR}}"
|
||||
TEST_FILTER="all" # "all" | "openssl" | "restore_backup"
|
||||
TEST_FILTER="all"
|
||||
WAIT_BOOT=false # set to true or pass --wait-boot to wait for emulator startup
|
||||
DEVICE_SERIAL="" # specific device serial; empty = use ANDROID_SERIAL or first device
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -204,8 +208,6 @@ ADB_BIN=$(which adb)
|
||||
|
||||
run_test() {
|
||||
local target="$1"
|
||||
local apk_dir="$BUILD_DIR/client/tests/android-build"
|
||||
local apk="$apk_dir/${target}.apk"
|
||||
local pkg_name="org.qtproject.example.${target}"
|
||||
local activity="${pkg_name}/org.qtproject.qt.android.bindings.QtActivity"
|
||||
|
||||
@@ -213,7 +215,27 @@ run_test() {
|
||||
echo ">>> Building $target APK..."
|
||||
cmake --build "$BUILD_DIR" --target "${target}_make_apk"
|
||||
|
||||
echo ">>> Installing $target APK on device..."
|
||||
local apk=""
|
||||
local candidates=(
|
||||
"$BUILD_DIR/client/tests/android-build/${target}.apk"
|
||||
"$BUILD_DIR/client/tests/api/android-build/build/outputs/apk/debug/android-build-debug.apk"
|
||||
)
|
||||
|
||||
for candidate in "${candidates[@]}"; do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
apk="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$apk" ]]; then
|
||||
echo "ERROR: APK not found for $target."
|
||||
echo "APK files found in build directory:"
|
||||
find "$BUILD_DIR/client/tests" -type f -name '*.apk' -print
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ">>> Installing $target APK on device: $apk"
|
||||
"$ADB_BIN" install -r "$apk"
|
||||
|
||||
# Pre-create files/stdout.txt before launching so the test process finds it
|
||||
@@ -262,6 +284,9 @@ case "$TEST_FILTER" in
|
||||
all)
|
||||
run_test test_android_openssl
|
||||
run_test test_android_restore_backup
|
||||
run_test test_ui_api_account_info_model
|
||||
run_test test_ui_subscription_controller
|
||||
run_test test_ui_services_model_and_controller
|
||||
;;
|
||||
openssl)
|
||||
run_test test_android_openssl
|
||||
@@ -269,8 +294,13 @@ case "$TEST_FILTER" in
|
||||
restore_backup)
|
||||
run_test test_android_restore_backup
|
||||
;;
|
||||
api)
|
||||
run_test test_ui_api_account_info_model
|
||||
run_test test_ui_subscription_controller
|
||||
run_test test_ui_services_model_and_controller
|
||||
;;
|
||||
*)
|
||||
echo "Unknown --test value: $TEST_FILTER (use 'all', 'openssl', or 'restore_backup')"
|
||||
echo "Unknown --test value: $TEST_FILTER (use 'all', 'openssl', 'restore_backup', or 'api')"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in new issue
Block a user