Compare commits

...
13 Commits
36 changed files with 5459 additions and 0 deletions

No files matched your search

+529
View File
@@ -5,8 +5,38 @@ on:
branches:
- '**'
# Self-hosted server tests (SelfHostedServerSetup, MultipleExports) install and
# remove docker containers on a single shared VPS: two workflow runs executing
# simultaneously corrupt each other's state (error 202, "No such container").
# Queue runs instead of running them in parallel; do not cancel the active one.
concurrency:
group: vps-tests
cancel-in-progress: false
env:
QT_MIRROR: https://mirrors.ocf.berkeley.edu/qt/ # https://download.qt.io/static/mirrorlist/
QTEST_FUNCTION_TIMEOUT: 7200000
SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY: ${{ secrets.SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY }}
THIRD_PARTY_AWG_VPN_KEY: ${{ secrets.THIRD_PARTY_AWG_VPN_KEY }}
THIRD_PARTY_WIRE_GUARD_VPN_KEY: ${{ secrets.THIRD_PARTY_WIRE_GUARD_VPN_KEY }}
THIRD_PARTY_XRAY_VPN_KEY: ${{ secrets.THIRD_PARTY_XRAY_VPN_KEY }}
THIRD_PARTY_OPEN_VPN_KEY: ${{ secrets.THIRD_PARTY_OPEN_VPN_KEY }}
THIRD_PARTY_CLOAK_VPN_KEY: ${{ secrets.THIRD_PARTY_CLOAK_VPN_KEY }}
THIRD_PARTY_SS_VPN_KEY: ${{ secrets.THIRD_PARTY_SS_VPN_KEY }}
THIRD_PARTY_IKEV_VPN_KEY: ${{ secrets.THIRD_PARTY_IKEV_VPN_KEY }}
THIRD_PARTY_PREMIUM_VPN_KEY: ${{ secrets.THIRD_PARTY_PREMIUM_VPN_KEY }}
THIRD_PARTY_VMESS_NEW_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_NEW_IMPORT_DATA }}
THIRD_PARTY_VMESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VMESS_IMPORT_DATA }}
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 }}
SELF_HOSTED_SERVER_HOST_NAME: ${{ secrets.SELF_HOSTED_SERVER_HOST_NAME }}
SELF_HOSTED_SERVER_USER_NAME: ${{ secrets.SELF_HOSTED_SERVER_USER_NAME }}
SELF_HOSTED_SERVER_PASSWORD: ${{ secrets.SELF_HOSTED_SERVER_PASSWORD }}
SELF_HOSTED_SERVER_SSH_PORT: ${{ secrets.SELF_HOSTED_SERVER_SSH_PORT }}
DNS_LIST_PATH: ${{ github.workspace }}/tests/data/dns_list.json
IP_LIST_PATH: ${{ github.workspace }}/tests/data/ip_list.json
jobs:
Detect-Changes:
@@ -120,6 +150,13 @@ jobs:
QT_INSTALL_DIR: ${{ runner.temp }}
run: deploy/build.sh --generator Ninja --installer all
- name: 'Upload Linux test build'
uses: actions/upload-artifact@v7
with:
name: linux-build
path: deploy/build/client/tests
retention-days: 7
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v7
with:
@@ -246,6 +283,15 @@ jobs:
set WIX_ROOT_PATH="${{ env.USERPROFILE }}\.dotnet\tools"
deploy\build.bat --installer all
- name: 'Upload Windows test build'
uses: actions/upload-artifact@v7
with:
name: windows-build
path: |
deploy/build/client/tests
deploy/build/client/Release
retention-days: 7
- name: 'Upload WIX installer artifact'
uses: actions/upload-artifact@v7
with:
@@ -539,6 +585,33 @@ jobs:
shell: bash
run: deploy/build.sh --generator Ninja --installer all
- name: 'Prepare macOS test runtime'
shell: bash
run: |
FRAMEWORKS_DIR=$(find deploy/build/_CPack_Packages \
-type d \
-path '*/AmneziaVPN.app/Contents/Frameworks' \
-print -quit)
echo "Frameworks directory: $FRAMEWORKS_DIR"
mkdir -p deploy/build/client/tests/openssl
cp "$FRAMEWORKS_DIR/libssl.3.dylib" \
deploy/build/client/tests/openssl/
cp "$FRAMEWORKS_DIR/libcrypto.3.dylib" \
deploy/build/client/tests/openssl/
ls -la deploy/build/client/tests/openssl
- name: 'Upload macOS test build'
uses: actions/upload-artifact@v7
with:
name: macos-build
path: deploy/build/client/tests
retention-days: 7
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v7
with:
@@ -1003,3 +1076,459 @@ jobs:
echo "Pull request:" >> $GITHUB_STEP_SUMMARY
echo "[[#${{ fromJSON(steps.pull_request.outputs.data)[0].number }}] ${{ fromJSON(steps.pull_request.outputs.data)[0].title }}](${{ fromJSON(steps.pull_request.outputs.data)[0].html_url }})" >> $GITHUB_STEP_SUMMARY
# ------------------------------------------------------
test-linux:
name: Test Linux (${{ matrix.filter }})
needs: Build-Linux-Ubuntu
strategy:
fail-fast: false
matrix:
filter:
- MultipleImportsTest
- ServerEditTest
- DefaultServerChangeTest
- ServerEdgeCasesTest
- SignalOrderTest
- ServersModelSyncTest
- ServerRenameTest
- SettingsSignalsTest
- UiServersModelAndControllerTest
- XraySerializationTest
- UiLanguageModelAndControllerTest
- UiIpModelAndControllerTest
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- ConnectionTest
- OpenSSLLibraryTest
runs-on: ubuntu-22.04
env:
QT_VERSION: 6.10.1
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- name: Decode backup file
if: matrix.filter == 'BackupExportImportTest'
shell: pwsh
run: |
$backup = Join-Path $env:RUNNER_TEMP "test.backup"
$bytes = [Convert]::FromBase64String("${{ secrets.BACKUP_FILE_BASE64 }}")
[IO.File]::WriteAllBytes($backup, $bytes)
"BACKUP_PATH=$backup" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Install Qt
uses: jurplel/install-qt-action@v3
with:
version: ${{ env.QT_VERSION }}
host: linux
target: desktop
arch: linux_gcc_64
modules: qtremoteobjects qt5compat qtshadertools
dir: ${{ runner.temp }}
set-env: true
aqtversion: '==3.3.0'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: Install system packages
run: |
sudo apt-get update
sudo apt-get install -y \
libxkbcommon-x11-0 \
libsecret-1-dev
- name: Download build
uses: actions/download-artifact@v4
with:
name: linux-build
path: deploy/build/client/tests
- name: Restore executable permissions
run: |
find deploy/build/client/tests \
-maxdepth 1 \
-type f \
-name "test_*" \
-exec chmod +x {} \;
- name: Run tests
shell: bash
run: |
QT_ROOT="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64"
export LD_LIBRARY_PATH="$QT_ROOT/lib:${LD_LIBRARY_PATH:-}"
export QT_QPA_PLATFORM=offscreen
ctest --test-dir deploy/build/client/tests --output-on-failure -VV -R "^${{ matrix.filter }}$"
# ------------------------------------------------------
test-windows:
name: Test Windows (${{ matrix.filter }})
needs: Build-Windows
strategy:
fail-fast: false
matrix:
filter:
- MultipleImportsTest
- ServerEditTest
- DefaultServerChangeTest
- ServerEdgeCasesTest
- SignalOrderTest
- ServersModelSyncTest
- ServerRenameTest
- SettingsSignalsTest
- UiServersModelAndControllerTest
- XraySerializationTest
- UiLanguageModelAndControllerTest
- UiIpModelAndControllerTest
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- ConnectionTest
- OpenSSLLibraryTest
runs-on: windows-latest
env:
QT_VERSION: 6.10.1
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- name: Decode backup file
if: matrix.filter == 'BackupExportImportTest'
shell: pwsh
run: |
$backup = Join-Path $env:RUNNER_TEMP "test.backup"
$bytes = [Convert]::FromBase64String("${{ secrets.BACKUP_FILE_BASE64 }}")
[IO.File]::WriteAllBytes($backup, $bytes)
"BACKUP_PATH=$backup" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Download build
uses: actions/download-artifact@v4
with:
name: windows-build
path: deploy/build/client
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: windows
target: desktop
arch: win64_msvc2022_64
modules: qtremoteobjects qt5compat qtshadertools
dir: ${{ runner.temp }}
set-env: true
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- name: Run tests
shell: cmd
run: |
rem Without an attached console (GitHub runner), QTest sends its output to
rem OutputDebugString instead of stdout, so ctest captures nothing.
set QT_FORCE_STDERR_LOGGING=1
set PATH=%PATH%;%CD%\client\3rd-prebuilt\deploy-prebuilt\windows\x64
set PATH=%PATH%;%Qt6_DIR%\..\..\..\bin
ctest --test-dir deploy\build\client\tests -C Release -R "${{ matrix.filter }}" --output-on-failure
# ------------------------------------------------------
test-macos:
name: Test macOS (${{ matrix.filter }})
needs: Build-MacOS
strategy:
fail-fast: false
matrix:
filter:
- MultipleImportsTest
- ServerEditTest
- DefaultServerChangeTest
- ServerEdgeCasesTest
- SignalOrderTest
- ServersModelSyncTest
- ServerRenameTest
- SettingsSignalsTest
- UiServersModelAndControllerTest
- XraySerializationTest
- UiLanguageModelAndControllerTest
- UiIpModelAndControllerTest
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- ConnectionTest
- OpenSSLLibraryTest
runs-on: macos-15
env:
QT_VERSION: 6.10.1
steps:
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.3.0"
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- name: Decode backup file
if: matrix.filter == 'BackupExportImportTest'
shell: pwsh
run: |
$backup = Join-Path $env:RUNNER_TEMP "test.backup"
$bytes = [Convert]::FromBase64String("${{ secrets.BACKUP_FILE_BASE64 }}")
[IO.File]::WriteAllBytes($backup, $bytes)
"BACKUP_PATH=$backup" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Download build
uses: actions/download-artifact@v4
with:
name: macos-build
path: deploy/build/client/tests
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: mac
target: desktop
arch: clang_64
modules: qtremoteobjects qt5compat qtshadertools
dir: ${{ runner.temp }}
set-env: true
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.14"
- name: Install Conan
run: pip install "conan==2.28.0"
- name: Install dependencies
run: |
brew update
brew install cmake ninja
- name: Restore executable permissions
run: |
find deploy/build/client/tests \
-maxdepth 1 \
-type f \
-name "test_*" \
-exec chmod +x {} \;
- name: Run tests
shell: bash
run: |
QT_ROOT="$RUNNER_TEMP/Qt/$QT_VERSION/macos"
export DYLD_LIBRARY_PATH="$PWD/deploy/build/client/tests/openssl:$QT_ROOT/lib:$DYLD_LIBRARY_PATH"
# export QT_QPA_PLATFORM=offscreen
ctest --test-dir deploy/build/client/tests -R "${{ matrix.filter }}" --output-on-failure
# ------------------------------------------------------
test-android:
runs-on: android-runner
env:
ANDROID_PLATFORM: android-28
NDK_VERSION: 27.0.11718014
QT_VERSION: 6.10.1
QT_MODULES: 'qtremoteobjects qt5compat qtshadertools'
steps:
- name: 'Get sources'
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 10
- name: 'Decode backup file'
run: |
echo "${{ secrets.BACKUP_FILE_BASE64 }}" | base64 --decode > /tmp/test.backup
echo "BACKUP_PATH=/tmp/test.backup" >> $GITHUB_ENV
- name: 'Install desktop Qt'
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
target: 'desktop'
arch: 'linux_gcc_64'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_x86_64 Qt'
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
target: 'android'
arch: 'android_x86_64'
modules: ${{ env.QT_MODULES }}
dir: ${{ runner.temp }}
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Setup Java'
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: 'Setup Android SDK'
uses: android-actions/setup-android@v4
with:
packages: "platforms;${{ env.ANDROID_PLATFORM }} ndk;${{ env.NDK_VERSION }}"
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: 'Install conan'
run: pip install "conan==2.28.0"
# Uncomment to sign APK with release keystore (requires secrets):
# - name: 'Decode keystore'
# env:
# 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
- 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: 'Verify Android test files'
run: |
echo "Runner script:"
grep 'local apk=' ci_run_android_tests.sh
echo
echo "APKs:"
find build -type f -name '*.apk' -print
- name: 'Run Android tests on emulator'
uses: reactivecircus/android-emulator-runner@v2
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
+6
View File
@@ -313,3 +313,9 @@ if(OVPN_SCRIPTS)
WORLD_READ WORLD_EXECUTE
)
endif()
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
add_subdirectory(tests)
+391
View File
@@ -0,0 +1,391 @@
find_package(Qt6 REQUIRED COMPONENTS Test)
include(CTest)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
qt6_add_resources(TEST_QRC
../server_scripts/serverScripts.qrc
)
set(_main_cpp "${CMAKE_CURRENT_SOURCE_DIR}/../main.cpp")
get_filename_component(_main_cpp "${_main_cpp}" REALPATH)
set(TEST_SOURCES ${SOURCES})
list(REMOVE_ITEM TEST_SOURCES "${_main_cpp}")
add_library(test_common OBJECT
${TEST_SOURCES}
${HEADERS}
../ui/utils/pageEnum.h
utils/testUtils.cpp
${TEST_QRC}
)
qt_add_repc_replicas(test_common
../../ipc/ipc_interface.rep
../../ipc/ipc_process_interface.rep
)
target_link_libraries(test_common PUBLIC
${LIBS}
)
target_include_directories(test_common PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
${CMAKE_CURRENT_BINARY_DIR}/..
${CMAKE_CURRENT_BINARY_DIR}
)
if(NOT ANDROID)
add_executable(test_multiple_imports
testMultipleImports.cpp
)
target_link_libraries(test_multiple_imports PRIVATE
Qt6::Test
test_common
)
add_executable(test_server_edit
testServerEdit.cpp
)
target_link_libraries(test_server_edit PRIVATE
Qt6::Test
test_common
)
add_executable(test_default_server_change
testDefaultServerChange.cpp
)
target_link_libraries(test_default_server_change PRIVATE
Qt6::Test
test_common
)
add_executable(test_server_edge_cases
testServerEdgeCases.cpp
)
target_link_libraries(test_server_edge_cases PRIVATE
Qt6::Test
test_common
)
add_executable(test_signal_order
testSignalOrder.cpp
)
target_link_libraries(test_signal_order PRIVATE
Qt6::Test
test_common
)
add_executable(test_servers_model_sync
testServersModelSync.cpp
)
target_link_libraries(test_servers_model_sync PRIVATE
Qt6::Test
test_common
)
add_executable(test_server_rename
testServerRename.cpp
)
target_link_libraries(test_server_rename PRIVATE
Qt6::Test
test_common
)
add_executable(test_settings_signals
testSettingsSignals.cpp
)
target_link_libraries(test_settings_signals PRIVATE
Qt6::Test
test_common
)
add_executable(test_ui_servers_model_and_controller
testUiServersModelAndController.cpp
)
target_link_libraries(test_ui_servers_model_and_controller PRIVATE
Qt6::Test
test_common
)
add_executable(test_xray_serialization
testXraySerialization.cpp
)
target_link_libraries(test_xray_serialization PRIVATE
Qt6::Test
test_common
)
add_executable(test_ui_language_model_and_controller
testUiLanguageModelAndController.cpp
)
target_link_libraries(test_ui_language_model_and_controller PRIVATE
Qt6::Test
test_common
)
add_executable(test_ui_ip_model_and_controller
testUiIpModelAndController.cpp
)
target_link_libraries(test_ui_ip_model_and_controller PRIVATE
Qt6::Test
test_common
)
add_executable(test_ui_app_st_model_and_controller
testUiAppSTModelAndController.cpp
)
target_link_libraries(test_ui_app_st_model_and_controller PRIVATE
Qt6::Test
test_common
)
add_executable(test_ui_allowed_dns_model_and_controller
testUiAllowedDnsModelAndController.cpp
)
target_link_libraries(test_ui_allowed_dns_model_and_controller PRIVATE
Qt6::Test
test_common
)
add_executable(test_backup_export_import
testBackupExportImport.cpp
)
target_link_libraries(test_backup_export_import PRIVATE
Qt6::Test
test_common
)
add_executable(test_connection
testConnection.cpp
)
target_link_libraries(test_connection PRIVATE
Qt6::Test
test_common
)
add_executable(test_openssl_library
testOpenSSLLibrary.cpp
)
target_link_libraries(test_openssl_library PRIVATE
Qt6::Test
test_common
)
if(WIN32)
# libcrypto-3-x64.dll is a hard import of this binary.
# Without copying Conan's DLL here, Windows loads the first match in PATH
# (MySQL, Git, PHP...) which differs from the Conan version we compiled against,
# causing testRuntimeVersionMatchesCompileTime and testWindowsNoDllHijack to fail.
foreach(_ssl_tgt OpenSSL::Crypto OpenSSL::SSL)
if(NOT TARGET "${_ssl_tgt}")
continue()
endif()
set(_ssl_dll "")
foreach(_prop IMPORTED_LOCATION IMPORTED_LOCATION_RELEASE IMPORTED_LOCATION_DEBUG
IMPORTED_LOCATION_MINSIZEREL IMPORTED_LOCATION_RELWITHDEBINFO
IMPORTED_LOCATION_NOCONFIG)
get_target_property(_candidate "${_ssl_tgt}" "${_prop}")
if(_candidate AND NOT _candidate MATCHES "-NOTFOUND" AND _candidate MATCHES "\\.dll$")
set(_ssl_dll "${_candidate}")
break()
endif()
endforeach()
if(_ssl_dll)
add_custom_command(TARGET test_openssl_library POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_ssl_dll}" "$<TARGET_FILE_DIR:test_openssl_library>"
)
else()
message(STATUS "No .dll found for ${_ssl_tgt} (IMPORTED_LOCATION may point to .lib) — skipping copy")
endif()
endforeach()
endif()
endif() # NOT ANDROID
if(ANDROID)
set(ANDROID_TEST_PACKAGE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android_test_package")
# Qt 6.x for Android ships its TLS plugin linked against libcrypto_3.so / libssl_3.so
# (Android convention: use underscore instead of dots for versioned sonames).
# Conan provides libcrypto.so.3 / libssl.so.3 with SONAME=libcrypto.so.3.
# We copy them with the _3.so suffix and patch the SONAME in-place.
# Both old ("libcrypto.so.3") and new ("libcrypto_3.so") are 14 bytes � exact fit.
find_package(Python3 REQUIRED COMPONENTS Interpreter)
find_package(OpenSSL REQUIRED)
function(_get_openssl_lib target outvar)
foreach(_prop IMPORTED_LOCATION IMPORTED_LOCATION_RELEASE IMPORTED_LOCATION_DEBUG IMPORTED_LOCATION_NOCONFIG)
get_target_property(_loc "${target}" "${_prop}")
if(_loc AND NOT _loc MATCHES "NOTFOUND")
set(${outvar} "${_loc}" PARENT_SCOPE)
return()
endif()
endforeach()
endfunction()
_get_openssl_lib(OpenSSL::Crypto _openssl_crypto_src)
_get_openssl_lib(OpenSSL::SSL _openssl_ssl_src)
message(STATUS "OpenSSL Crypto src: ${_openssl_crypto_src}")
message(STATUS "OpenSSL SSL src: ${_openssl_ssl_src}")
set(_openssl_crypto_dst "${CMAKE_CURRENT_BINARY_DIR}/libcrypto_3.so")
set(_openssl_ssl_dst "${CMAKE_CURRENT_BINARY_DIR}/libssl_3.so")
configure_file("${_openssl_crypto_src}" "${_openssl_crypto_dst}" COPYONLY)
configure_file("${_openssl_ssl_src}" "${_openssl_ssl_dst}" COPYONLY)
# Patch SONAME bytes in our copies (in-place string replacement in .dynstr).
# libcrypto.so.3 → libcrypto_3.so (14 chars each, no padding needed)
# libssl.so.3 → libssl_3.so (11 chars each, no padding needed)
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/patch_soname.py" [=[
import sys
def patch_all(path, old, new):
assert len(old) == len(new), f"SONAME lengths differ: {old!r} vs {new!r}"
old_b = old.encode() + b'\x00'
new_b = new.encode() + b'\x00'
with open(path, 'rb') as f:
data = bytearray(f.read())
count, idx = 0, 0
while True:
idx = data.find(old_b, idx)
if idx < 0:
break
data[idx:idx + len(old_b)] = new_b
count += 1
idx += len(new_b)
if count:
with open(path, 'wb') as f:
f.write(data)
print(f' patched {count}x "{old}" -> "{new}" in {path}')
else:
print(f' WARNING: "{old}" not found in {path} (already patched?)')
# libcrypto: SONAME libcrypto.so.3 → libcrypto_3.so (both 14 chars)
patch_all(sys.argv[1], 'libcrypto.so.3', 'libcrypto_3.so')
# libssl: SONAME libssl.so.3 → libssl_3.so (both 11 chars)
patch_all(sys.argv[2], 'libssl.so.3', 'libssl_3.so')
# libssl DT_NEEDED on libcrypto: libcrypto.so.3 → libcrypto_3.so
patch_all(sys.argv[2], 'libcrypto.so.3', 'libcrypto_3.so')
print('SONAME patch done.')
]=])
execute_process(
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_BINARY_DIR}/patch_soname.py"
"${_openssl_crypto_dst}" "${_openssl_ssl_dst}"
RESULT_VARIABLE _patch_result
OUTPUT_VARIABLE _patch_output
ERROR_VARIABLE _patch_error
)
message(STATUS "SONAME patch output: ${_patch_output}")
if(NOT _patch_result EQUAL 0)
message(FATAL_ERROR "SONAME patch failed:\n${_patch_error}")
endif()
# Redirect all link targets so tests get DT_NEEDED:libcrypto_3.so, not libcrypto.so.3.
# Must override every config-specific variant because CMake prefers
# IMPORTED_LOCATION_DEBUG over IMPORTED_LOCATION for Debug builds.
foreach(_ssl_target OpenSSL::Crypto OpenSSL::SSL)
if(_ssl_target STREQUAL "OpenSSL::Crypto")
set(_ssl_dst "${_openssl_crypto_dst}")
else()
set(_ssl_dst "${_openssl_ssl_dst}")
endif()
foreach(_prop IMPORTED_LOCATION IMPORTED_LOCATION_DEBUG IMPORTED_LOCATION_RELEASE
IMPORTED_LOCATION_MINSIZEREL IMPORTED_LOCATION_RELWITHDEBINFO
IMPORTED_LOCATION_NOCONFIG)
get_target_property(_cur "${_ssl_target}" "${_prop}")
if(_cur AND NOT _cur MATCHES "NOTFOUND")
set_property(TARGET "${_ssl_target}" PROPERTY "${_prop}" "${_ssl_dst}")
endif()
endforeach()
endforeach()
qt_add_executable(test_android_openssl
testAndroidOpenSSL.cpp
)
target_link_libraries(test_android_openssl PRIVATE
Qt6::Core
Qt6::Test
Qt6::Gui
Qt6::Network
)
target_include_directories(test_android_openssl PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
)
set_target_properties(test_android_openssl PROPERTIES
QT_ANDROID_PACKAGE_SOURCE_DIR "${ANDROID_TEST_PACKAGE_DIR}"
QT_ANDROID_EXTRA_LIBS "${_openssl_crypto_dst};${_openssl_ssl_dst}"
)
qt_add_executable(test_android_restore_backup
testAndroidRestoreBackup.cpp
)
target_link_libraries(test_android_restore_backup PRIVATE
Qt6::Test
test_common
)
set_target_properties(test_android_restore_backup PROPERTIES
QT_ANDROID_PACKAGE_SOURCE_DIR "${ANDROID_TEST_PACKAGE_DIR}"
QT_ANDROID_EXTRA_LIBS "${_openssl_crypto_dst};${_openssl_ssl_dst}"
)
endif()
if(NOT ANDROID)
add_subdirectory(selfHostedAdmin)
add_subdirectory(api)
endif()
enable_testing()
if(NOT ANDROID)
add_test(NAME MultipleImportsTest COMMAND test_multiple_imports)
add_test(NAME ServerEditTest COMMAND test_server_edit)
add_test(NAME DefaultServerChangeTest COMMAND test_default_server_change)
add_test(NAME ServerEdgeCasesTest COMMAND test_server_edge_cases)
add_test(NAME SignalOrderTest COMMAND test_signal_order)
add_test(NAME ServersModelSyncTest COMMAND test_servers_model_sync)
add_test(NAME ServerRenameTest COMMAND test_server_rename)
add_test(NAME SettingsSignalsTest COMMAND test_settings_signals)
add_test(NAME UiServersModelAndControllerTest COMMAND test_ui_servers_model_and_controller)
add_test(NAME XraySerializationTest COMMAND test_xray_serialization)
add_test(NAME UiLanguageModelAndControllerTest COMMAND test_ui_language_model_and_controller)
add_test(NAME UiIpModelAndControllerTest COMMAND test_ui_ip_model_and_controller)
add_test(NAME UiAppSTModelAndControllerTest COMMAND test_ui_app_st_model_and_controller)
add_test(NAME UiAllowedDnsModelAndControllerTest COMMAND test_ui_allowed_dns_model_and_controller)
add_test(NAME BackupExportImportTest COMMAND test_backup_export_import)
add_test(NAME ConnectionTest COMMAND test_connection)
add_test(NAME OpenSSLLibraryTest COMMAND test_openssl_library)
endif()
# Android tests are executed on-device via androidtestrunner in CI (see .github/workflows/tests.yml).
# CTest cannot run Android binaries on the host machine, so no add_test() entries are registered here.
@@ -0,0 +1,55 @@
<?xml version="1.0"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.qtproject.example"
android:installLocation="auto"
android:versionCode="-- %%INSERT_VERSION_CODE%% --"
android:versionName="-- %%INSERT_VERSION_NAME%% --">
<!-- %%INSERT_PERMISSIONS -->
<!-- %%INSERT_FEATURES -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true" />
<application
android:name="org.qtproject.qt.android.bindings.QtApplication"
android:hardwareAccelerated="false"
android:label="-- %%INSERT_APP_NAME%% --"
android:icon="-- %%INSERT_APP_ICON%% --"
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:fullBackupOnly="false">
<activity
android:name="org.qtproject.qt.android.bindings.QtActivity"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
android:label="-- %%INSERT_APP_NAME%% --"
android:launchMode="singleTop"
android:screenOrientation="unspecified"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="-- %%INSERT_APP_LIB_NAME%% --" />
<meta-data
android:name="android.app.arguments"
android:value="-- %%INSERT_APP_ARGUMENTS%% --" />
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.qtprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/qtprovider_paths"/>
</provider>
</application>
</manifest>
+20
View File
@@ -0,0 +1,20 @@
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
)
target_link_libraries(test_ui_news_model_and_controller PRIVATE
Qt6::Test
test_common
)
add_test(NAME UiApiServicesModelAndControllerTest COMMAND test_ui_api_services_model_and_controller)
add_test(NAME UiNewsModelAndControllerTest COMMAND test_ui_news_model_and_controller)
@@ -0,0 +1,97 @@
#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"
@@ -0,0 +1,97 @@
#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 TestUiNewsModelAndController : 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 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");
QModelIndex newsModelIndex = m_coreController->m_newsModel->index(0, 0);
QVERIFY2(newsModelIndex.isValid(), "News model index should be valid");
auto newsId = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::IdRole);
QCOMPARE(newsId, );
auto newsTitle = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::TitleRole);
QCOMPARE(newsTitle, );
auto newsContent = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::ContentRole);
QCOMPARE(newsContent, );
auto newsTimestamp = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::TimestampRole);
QCOMPARE(newsTimestamp, );
auto newsIsRead = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::IsReadRole);
QCOMPARE(newsIsRead, false);
auto newsIsProcessed = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::IsProcessedRole);
QCOMPARE(newsIsProcessed, );
m_coreController->m_newsModel->markAsRead(0);
? m_coreController->m_newsModel->updateModel(); ?
QVERIFY(hasUnreadChangedSpy.count() == 2, "hasUnreadChanged signal should be emitted");
QCOMPARE(newsIsRead, true);
*/
}
};
QTEST_MAIN(TestUiNewsModelAndController)
#include "testUiNewsModelAndController.moc"
+1
View File
@@ -0,0 +1 @@
["1.1.1.1", "8.8.8.8", "9.9.9.9"]
+1
View File
@@ -0,0 +1 @@
[{"hostname": "1.2.3.4"}, {"hostname": "5.6.7.8"}, {"hostname": "10.20.30.40"}]
@@ -0,0 +1,31 @@
add_executable(test_admin_self_hosted_export
testAdminSelfHostedExport.cpp
)
target_link_libraries(test_admin_self_hosted_export PRIVATE
Qt6::Test
test_common
)
add_executable(test_self_hosted_server_setup
testSelfHostedServerSetup.cpp
)
target_link_libraries(test_self_hosted_server_setup PRIVATE
Qt6::Test
test_common
)
add_executable(test_multiple_exports
testMultipleExports.cpp
)
target_link_libraries(test_multiple_exports PRIVATE
Qt6::Test
test_common
)
add_test(NAME AdminSelfHostedExportTest COMMAND test_admin_self_hosted_export)
add_test(NAME SelfHostedServerSetupTest COMMAND test_self_hosted_server_setup)
add_test(NAME MultipleExportsTest COMMAND test_multiple_exports)
@@ -0,0 +1,206 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QFile>
#include <QDebug>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/utils/constants/configKeys.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia::test;
class TestAdminSelfHostedExport : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
QJsonObject decodeVpnKey(const QString &vpnKey) {
QString key = vpnKey;
key.replace("vpn://", "");
QByteArray ba = QByteArray::fromBase64(
key.toUtf8(),
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals
);
qDebug() << "Base64 decoded size:" << ba.size();
QJsonDocument testDoc = QJsonDocument::fromJson(ba);
if (!testDoc.isNull()) {
qDebug() << "Data is not compressed, using as-is";
return testDoc.object();
}
QByteArray baUncompressed = qUncompress(ba);
if (!baUncompressed.isEmpty()) {
qDebug() << "Data was compressed, uncompressed size:" << baUncompressed.size();
ba = baUncompressed;
} else {
qDebug() << "qUncompress failed or data is not compressed";
}
return QJsonDocument::fromJson(ba).object();
}
// Checks that every value present in `expected` exists in `actual` unchanged
// (recursively). Extra keys in `actual` are allowed: a newer client adds
// default fields on export (e.g. xray xhttp/mkcp) that are absent in a key
// generated by an older version. Losing or changing imported data still fails.
static bool isJsonSubset(const QJsonValue &expected, const QJsonValue &actual, QString &path) {
if (expected.isObject()) {
if (!actual.isObject()) {
path += " (not an object)";
return false;
}
const QJsonObject expObj = expected.toObject();
const QJsonObject actObj = actual.toObject();
for (auto it = expObj.constBegin(); it != expObj.constEnd(); ++it) {
QString subPath = path + "/" + it.key();
if (!actObj.contains(it.key())) {
path = subPath + " (missing)";
return false;
}
if (!isJsonSubset(it.value(), actObj.value(it.key()), subPath)) {
path = subPath;
return false;
}
}
return true;
}
if (expected.isArray()) {
if (!actual.isArray()) {
path += " (not an array)";
return false;
}
const QJsonArray expArr = expected.toArray();
const QJsonArray actArr = actual.toArray();
if (expArr.size() != actArr.size()) {
path += QString(" (array size %1 != %2)").arg(expArr.size()).arg(actArr.size());
return false;
}
for (int i = 0; i < expArr.size(); ++i) {
QString subPath = path + QString("/[%1]").arg(i);
if (!isJsonSubset(expArr.at(i), actArr.at(i), subPath)) {
path = subPath;
return false;
}
}
return true;
}
if (expected != actual) {
path += " (value differs)";
return false;
}
return true;
}
QJsonObject sortContainers(const QJsonObject &config) {
QJsonObject sorted = config;
if (!config.contains("containers")) {
return sorted;
}
QJsonArray containers = config["containers"].toArray();
QVector<QJsonObject> containerVec;
for (const QJsonValue &val : containers) {
containerVec.append(val.toObject());
}
std::sort(containerVec.begin(), containerVec.end(), [](const QJsonObject &a, const QJsonObject &b) {
return a["container"].toString() < b["container"].toString();
});
QJsonArray sortedContainers;
for (const QJsonObject &obj : containerVec) {
sortedContainers.append(obj);
}
sorted["containers"] = sortedContainers;
return sorted;
}
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();
}
void testAdminSelfHostedExport() {
QString vpnKey = getEnvValue("SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY");
logEnvValueState("SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY");
if (!isEnvValueConfigured(vpnKey)) {
QSKIP("Set SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY");
}
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(vpnKey);
QVERIFY2(importResult.errorCode == ErrorCode::NoError, "Import should succeed");
QVERIFY2(!importResult.config.isEmpty(), "Config should not be empty");
QJsonObject importedConfig = importResult.config;
m_coreController->m_importCoreController->importConfig(importedConfig);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
const QString serverId = m_coreController->m_serversRepository->defaultServerId();
auto exportResult = m_coreController->m_exportController->generateFullAccessConfig(serverId);
QVERIFY2(exportResult.errorCode == ErrorCode::NoError, "Export should succeed");
QVERIFY2(!exportResult.config.isEmpty(), "Exported config should not be empty");
qDebug() << "EXPORTED KEY:" << exportResult.config;
QJsonObject exportedConfig = decodeVpnKey(exportResult.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(exportResult.config);
QVERIFY2(importResult2.errorCode == ErrorCode::NoError, "Re-import should succeed");
QJsonObject sortedImported = sortContainers(importedConfig);
QJsonObject sortedExported = sortContainers(importResult2.config);
QString importedJson = QJsonDocument(sortedImported).toJson(QJsonDocument::Compact);
QString exportedJson = QJsonDocument(sortedExported).toJson(QJsonDocument::Compact);
qDebug() << "IMPORTED JSON:" << importedJson;
qDebug() << "EXPORTED JSON:" << exportedJson;
QString divergedPath;
QVERIFY2(isJsonSubset(sortedImported, sortedExported, divergedPath),
qPrintable("Exported config lost or changed imported data at: " + divergedPath));
}
};
QTEST_MAIN(TestAdminSelfHostedExport)
#include "testAdminSelfHostedExport.moc"
@@ -0,0 +1,266 @@
#include <QDebug>
#include <QUuid>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/commonStructs.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include <libssh/libssh.h>
#include "secureQSettings.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
using namespace amnezia;
using namespace amnezia::test;
namespace {
enum class NativeExportKind {
Awg = 1,
WireGuard,
OpenVpn,
Xray,
};
QList<DockerContainer> vpnContainersForExport()
{
return {
DockerContainer::Awg2,
DockerContainer::WireGuard,
DockerContainer::OpenVpn,
DockerContainer::Xray
};
}
} // namespace
class TestMultipleExports : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
QString m_serverId;
ServerCredentials getCredentialsFromIni()
{
ServerCredentials credentials;
credentials.hostName = getEnvValue("SELF_HOSTED_SERVER_HOST_NAME");
credentials.userName = getEnvValue("SELF_HOSTED_SERVER_USER_NAME");
credentials.secretData = getEnvValue("SELF_HOSTED_SERVER_PASSWORD");
credentials.port = getEnvValue("SELF_HOSTED_SERVER_SSH_PORT").toInt();
logEnvValueState("SELF_HOSTED_SERVER_HOST_NAME");
logEnvValueState("SELF_HOSTED_SERVER_USER_NAME");
logEnvValueState("SELF_HOSTED_SERVER_PASSWORD");
logEnvValueState("SELF_HOSTED_SERVER_SSH_PORT");
return credentials;
}
int portForContainer(DockerContainer container) const
{
const Proto proto = ContainerUtils::defaultProtocol(container);
if (container == DockerContainer::Awg2) {
return ProtocolUtils::getPortForInstall(proto);
}
if (container == DockerContainer::Xray) {
// The default 443 is occupied on the test VPS
// (installContainer fails with ServerPortAlreadyAllocatedError).
return 8443;
}
return ProtocolUtils::defaultPort(proto);
}
TransportProto transportForContainer(DockerContainer container) const
{
return ProtocolUtils::defaultTransportProto(ContainerUtils::defaultProtocol(container));
}
void installVpnContainer(DockerContainer container)
{
const QString containerName = ContainerUtils::containerToString(container);
bool wasInstalled = false;
const ErrorCode error = m_coreController->m_installController->installContainer(
m_serverId, container, portForContainer(container), transportForContainer(container), wasInstalled);
QVERIFY2(error == ErrorCode::NoError,
QString("%1: installContainer should succeed. Error: %2")
.arg(containerName)
.arg(static_cast<int>(error))
.toUtf8()
.constData());
qDebug() << containerName << "installed:" << wasInstalled;
}
void setupServerWithVpnContainers()
{
ServerCredentials credentials = getCredentialsFromIni();
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
QSKIP("Set SELF_HOSTED_SERVER_HOST_NAME, SELF_HOSTED_SERVER_USER_NAME, SELF_HOSTED_SERVER_PASSWORD");
}
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName
<< "port:" << credentials.port;
QString sshOutput;
const ErrorCode sshError =
m_coreController->m_installController->checkSshConnection(credentials, sshOutput);
QVERIFY2(sshError == ErrorCode::NoError,
QString("SSH connection should succeed. Error: %1, Output: %2")
.arg(static_cast<int>(sshError))
.arg(sshOutput)
.toUtf8()
.constData());
m_coreController->m_installController->addEmptyServer(credentials);
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
m_serverId = m_coreController->m_serversRepository->defaultServerId();
QVERIFY2(!m_serverId.isEmpty(), "Server id should not be empty");
const ErrorCode removeError = m_coreController->m_installController->removeAllContainers(m_serverId);
QVERIFY2(removeError == ErrorCode::NoError,
QString("removeAllContainers should succeed. Error: %1")
.arg(static_cast<int>(removeError))
.toUtf8()
.constData());
qDebug() << "Remote Amnezia services cleared";
for (DockerContainer container : vpnContainersForExport()) {
installVpnContainer(container);
}
qDebug() << "All VPN containers installed for export tests";
}
void skipIfContainerNotInstalled(const QString &containerName, int containerIndex)
{
if (!m_coreController->m_containersModel->data(containerIndex, ContainersModel::Roles::IsInstalledRole).toBool()) {
const QString reason = QString("%1: Not installed").arg(containerName);
QSKIP(reason.toUtf8().constData());
}
}
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);
ssh_init();
setupServerWithVpnContainers();
}
void cleanupTestCase()
{
if (!m_serverId.isEmpty()) {
m_coreController->m_installController->removeAllContainers(m_serverId);
/* Raises critical error because cant clear clientTable
for (int containerIndex = 1; containerIndex < 7; ++containerIndex) {
m_coreController->m_installUiController->clearCachedProfile(m_serverId, containerIndex);
}
*/
m_coreController->m_serversController->removeServer(m_serverId);
qDebug() << "SERVER REMOVED\n";
}
ssh_finalize();
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void testMultipleExports_data()
{
QTest::addColumn<QString>("containerName");
QTest::addColumn<int>("containerIndex");
QTest::newRow("awg2") << "Awg2" << static_cast<int>(DockerContainer::Awg2);
QTest::newRow("wireguard") << "WireGuard" << static_cast<int>(DockerContainer::WireGuard);
QTest::newRow("openvpn") << "OpenVPN" << static_cast<int>(DockerContainer::OpenVpn);
QTest::newRow("xray") << "XRay" << static_cast<int>(DockerContainer::Xray);
}
void testMultipleExports()
{
QFETCH(QString, containerName);
QFETCH(int, containerIndex);
skipIfContainerNotInstalled(containerName, containerIndex);
const QString clientName = "MultipleExports Test Client";
const auto exportResult =
m_coreController->m_exportController->generateConnectionConfig(m_serverId, containerIndex, clientName);
QVERIFY2(exportResult.errorCode == ErrorCode::NoError,
QString("\n%1: Export should succeed").arg(containerName).toUtf8().constData());
QVERIFY2(!exportResult.config.isEmpty(),
QString("%1: Exported config should not be empty").arg(containerName).toUtf8().constData());
}
void testMultipleExportsNative_data()
{
QTest::addColumn<QString>("containerName");
QTest::addColumn<int>("containerIndex");
QTest::addColumn<int>("nativeExportKind");
QTest::newRow("awg") << "Awg2" << static_cast<int>(DockerContainer::Awg2)
<< static_cast<int>(NativeExportKind::Awg);
QTest::newRow("wireguard") << "WireGuard" << static_cast<int>(DockerContainer::WireGuard)
<< static_cast<int>(NativeExportKind::WireGuard);
QTest::newRow("openvpn") << "OpenVPN" << static_cast<int>(DockerContainer::OpenVpn)
<< static_cast<int>(NativeExportKind::OpenVpn);
QTest::newRow("xray") << "XRay" << static_cast<int>(DockerContainer::Xray)
<< static_cast<int>(NativeExportKind::Xray);
}
void testMultipleExportsNative()
{
QFETCH(QString, containerName);
QFETCH(int, containerIndex);
QFETCH(int, nativeExportKind);
skipIfContainerNotInstalled(containerName, containerIndex);
const QString clientName = "MultipleExports Test Client";
ExportController::ExportResult exportResult;
switch (static_cast<NativeExportKind>(nativeExportKind)) {
case NativeExportKind::Awg:
exportResult = m_coreController->m_exportController->generateAwgConfig(m_serverId, containerIndex, clientName);
break;
case NativeExportKind::WireGuard:
exportResult = m_coreController->m_exportController->generateWireGuardConfig(m_serverId, clientName);
break;
case NativeExportKind::OpenVpn:
exportResult = m_coreController->m_exportController->generateOpenVpnConfig(m_serverId, clientName);
break;
case NativeExportKind::Xray:
exportResult = m_coreController->m_exportController->generateXrayConfig(m_serverId, clientName);
break;
}
QVERIFY2(exportResult.errorCode == ErrorCode::NoError,
QString("\n%1 (native): Export should succeed").arg(containerName).toUtf8().constData());
QVERIFY2(!exportResult.config.isEmpty(),
QString("%1 (native): Exported config should not be empty").arg(containerName).toUtf8().constData());
}
};
QTEST_MAIN(TestMultipleExports)
#include "testMultipleExports.moc"
@@ -0,0 +1,397 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QProcessEnvironment>
#include <QDebug>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/awgProtocolConfig.h"
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/utils/commonStructs.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/errorCodes.h"
#include "ui/models/serversModel.h"
#include <libssh/libssh.h>
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestSelfHostedServerSetup : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
ServerCredentials getCredentialsFromEnv() {
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
QString hostName = getEnvValue("SELF_HOSTED_SERVER_HOST_NAME");
QString userName = getEnvValue("SELF_HOSTED_SERVER_USER_NAME");
QString password = getEnvValue("SELF_HOSTED_SERVER_PASSWORD");
QString portStr = getEnvValue("SELF_HOSTED_SERVER_SSH_PORT");
int port = portStr.toInt();
logEnvValueState("SELF_HOSTED_SERVER_HOST_NAME");
logEnvValueState("SELF_HOSTED_SERVER_USER_NAME");
logEnvValueState("SELF_HOSTED_SERVER_PASSWORD");
logEnvValueState("SELF_HOSTED_SERVER_SSH_PORT");
ServerCredentials credentials;
credentials.hostName = hostName;
credentials.userName = userName;
credentials.secretData = password;
credentials.port = port;
return credentials;
}
void verifySshConnection(ServerCredentials credentials) {
QString sshOutput;
ErrorCode sshError = m_coreController->m_installController->checkSshConnection(credentials, sshOutput);
QVERIFY2(sshError == ErrorCode::NoError,
QString("SSH connection should succeed. Error: %1, Output: %2")
.arg(static_cast<int>(sshError))
.arg(sshOutput)
.toUtf8().constData());
qDebug() << "SSH connection successful. Output:" << sshOutput;
}
void verifyAdminAccess(int serverIndex)
{
const QString serverId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
const auto adminCfg = m_coreController->m_serversRepository->selfHostedAdminConfig(serverId);
QVERIFY2(adminCfg.has_value(), "Server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig &selfHosted = *adminCfg;
QVERIFY2(selfHosted.hasCredentials(),
"Server should have credentials (admin access)");
QVERIFY2(!selfHosted.userName.isEmpty(),
"Server should have userName for admin access");
QVERIFY2(!selfHosted.password.isEmpty(),
"Server should have password for admin access");
QVERIFY2(!selfHosted.isReadOnly(),
"Server should not be read-only (should have admin access)");
if (m_coreController->m_serversModel) {
bool hasWriteAccess = m_coreController->m_serversModel->data(
m_coreController->m_serversModel->index(serverIndex, 0),
ServersModel::HasWriteAccessRole
).toBool();
QVERIFY2(hasWriteAccess,
"Server should have write access (admin access) according to ServersModel");
}
qDebug() << "Admin access verified for server at index:" << serverIndex;
}
void verifyClientConfig(const ContainerConfig& containerConfig, DockerContainer container) {
QString containerName = ContainerUtils::containerToString(container);
qDebug() << "Checking container:" << containerName;
if (ContainerUtils::containerService(container) != ServiceType::Other) {
bool hasClientConfig = containerConfig.protocolConfig.hasClientConfig();
QVERIFY2(hasClientConfig,
QString("Container %1 should have client config initialized")
.arg(containerName)
.toUtf8().constData());
if (container == DockerContainer::Awg) {
const AwgProtocolConfig* awgProtocolConfig = containerConfig.protocolConfig.as<AwgProtocolConfig>();
QVERIFY2(awgProtocolConfig != nullptr, "Protocol config should be AwgProtocolConfig");
QVERIFY2(awgProtocolConfig->hasClientConfig(), "AwgProtocolConfig should have client config");
const std::optional<AwgClientConfig>& clientCfgOpt = awgProtocolConfig->clientConfig;
QVERIFY2(clientCfgOpt.has_value(), "Awg client config should exist");
const AwgClientConfig& awgClientConfig = *clientCfgOpt;
QVERIFY2(!awgClientConfig.hostName.isEmpty(), "Awg client config should have hostName");
QVERIFY2(awgClientConfig.port > 0, "Awg client config should have valid port");
QVERIFY2(!awgClientConfig.clientPrivateKey.isEmpty(), "Awg client config should have clientPrivateKey");
QVERIFY2(!awgClientConfig.clientPublicKey.isEmpty(), "Awg client config should have clientPublicKey");
QVERIFY2(!awgClientConfig.serverPublicKey.isEmpty(), "Awg client config should have serverPublicKey");
QVERIFY2(!awgClientConfig.clientId.isEmpty(), "Awg client config should have clientId");
QVERIFY2(!awgClientConfig.nativeConfig.isEmpty(), "Awg client config should have nativeConfig");
}
qDebug() << "Container" << containerName << "has valid client config initialized";
} else {
qDebug() << "Container" << containerName << "is service type Other, skipping client config check";
}
}
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);
ssh_init();
}
void cleanupTestCase() {
ssh_finalize();
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 testSelfHostedServerSetup() {
ServerCredentials credentials = getCredentialsFromEnv();
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
QSKIP("Set SELF_HOSTED_SERVER_HOST_NAME, SELF_HOSTED_SERVER_USER_NAME, SELF_HOSTED_SERVER_PASSWORD");
}
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
verifySshConnection(credentials);
int awgPort = 55424;
TransportProto awgTransportProto = TransportProto::Udp;
bool wasAwgInstalled = false;
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
ErrorCode installServerError = m_coreController->m_installController->installServer(
credentials, DockerContainer::Awg, awgPort, awgTransportProto, wasAwgInstalled);
QVERIFY2(installServerError == ErrorCode::NoError,
QString("installServer for Awg should succeed. Error: %1")
.arg(static_cast<int>(installServerError))
.toUtf8().constData());
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
qDebug() << "Server with Awg container added at index:" << serverIndex;
const auto adminAfterAwg = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(adminAfterAwg.has_value(), "Server should be self-hosted (admin)");
const SelfHostedAdminServerConfig *selfHostedAfterAwg = &(*adminAfterAwg);
QVERIFY2(selfHostedAfterAwg->defaultContainer == DockerContainer::Awg, "Default container should be Awg");
QVERIFY2(selfHostedAfterAwg->containers.contains(DockerContainer::Awg), "Server should have Awg container");
ContainerConfig awgConfig = selfHostedAfterAwg->containers.value(DockerContainer::Awg);
QVERIFY2(awgConfig.container == DockerContainer::Awg, "Awg container config should be valid");
QVERIFY2(selfHostedAfterAwg->containers.size() == 1,
QString("Server should have exactly 1 container after Awg installation, but has %1")
.arg(selfHostedAfterAwg->containers.size())
.toUtf8().constData());
verifyClientConfig(awgConfig, DockerContainer::Awg);
qDebug() << "Awg container installed and configured successfully with valid client config";
int dnsPort = 53;
TransportProto dnsTransportProto = TransportProto::Udp;
bool wasDnsInstalled = false;
const QString serverIdForOps = m_coreController->m_serversRepository->serverIdAt(serverIndex);
ErrorCode installContainerError = m_coreController->m_installController->installContainer(
serverIdForOps, DockerContainer::Dns, dnsPort, dnsTransportProto, wasDnsInstalled);
QVERIFY2(installContainerError == ErrorCode::NoError,
QString("installContainer for Dns should succeed. Error: %1")
.arg(static_cast<int>(installContainerError))
.toUtf8().constData());
qDebug() << "Dns container installed:" << wasDnsInstalled;
const auto adminAfterDns = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(adminAfterDns.has_value(), "Server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig *selfHostedAfterDns = &(*adminAfterDns);
QVERIFY2(selfHostedAfterDns->containers.contains(DockerContainer::Awg), "Server should still have Awg container");
QVERIFY2(selfHostedAfterDns->containers.contains(DockerContainer::Dns), "Server should have Dns container");
QVERIFY2(selfHostedAfterDns->containers.size() == 2,
QString("Server should have exactly 2 containers after Dns installation, but has %1")
.arg(selfHostedAfterDns->containers.size())
.toUtf8().constData());
ContainerConfig dnsConfig = selfHostedAfterDns->containers.value(DockerContainer::Dns);
QVERIFY2(dnsConfig.container == DockerContainer::Dns, "Dns container config should be valid");
const DnsProtocolConfig* dnsProtocolConfig = dnsConfig.protocolConfig.as<DnsProtocolConfig>();
QVERIFY2(dnsProtocolConfig != nullptr, "Protocol config should be DnsProtocolConfig");
qDebug() << "Dns container installed and configured successfully";
verifyAdminAccess(serverIndex);
qDebug() << "Test completed successfully. Server setup with Awg and Dns containers is complete.";
}
void testSelfHostedServerEmptyRecover() {
ServerCredentials credentials = getCredentialsFromEnv();
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
QSKIP("Set SELF_HOSTED_SERVER_HOST_NAME, SELF_HOSTED_SERVER_USER_NAME, SELF_HOSTED_SERVER_PASSWORD");
}
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
verifySshConnection(credentials);
SelfHostedAdminServerConfig serverConfig;
serverConfig.hostName = credentials.hostName;
serverConfig.userName = credentials.userName;
serverConfig.password = credentials.secretData;
serverConfig.port = credentials.port;
serverConfig.description = m_coreController->m_serversRepository->nextAvailableServerName();
serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description;
serverConfig.defaultContainer = DockerContainer::None;
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
m_coreController->m_serversRepository->addServer(QString(), serverConfig.toJson(),
serverConfigUtils::ConfigType::SelfHostedAdmin);
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
qDebug() << "Empty server added at index:" << serverIndex;
const auto addedAdmin = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(addedAdmin.has_value(), "Added server should be self-hosted admin");
const SelfHostedAdminServerConfig *selfHosted = &(*addedAdmin);
QVERIFY2(selfHosted->containers.isEmpty(), "Server should have no containers initially");
QVERIFY2(selfHosted->defaultContainer == DockerContainer::None, "Default container should be None");
const QString scanServerId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
ErrorCode scanError = m_coreController->m_installController->scanServerForInstalledContainers(scanServerId);
QVERIFY2(scanError == ErrorCode::NoError,
QString("Server scan should succeed. Error: %1")
.arg(static_cast<int>(scanError))
.toUtf8().constData());
qDebug() << "Server scan completed successfully";
const auto scannedAdmin = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(scannedAdmin.has_value(), "Scanned server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig *scannedSelfHosted = &(*scannedAdmin);
QMap<DockerContainer, ContainerConfig> containers = scannedSelfHosted->containers;
int containersCount = containers.size();
qDebug() << "Found containers count:" << containersCount;
QVERIFY2(containersCount >= 0,
QString("Containers count should be non-negative, but got %1")
.arg(containersCount)
.toUtf8().constData());
if (containersCount > 0) {
qDebug() << "Server has" << containersCount << "installed container(s)";
} else {
qDebug() << "Server has no installed containers";
}
for (auto it = containers.begin(); it != containers.end(); ++it) {
verifyClientConfig(it.value(), it.key());
}
QVERIFY2(scannedSelfHosted->containers.size() == containersCount,
QString("Scanned containers count should match. Expected: %1, Actual: %2")
.arg(containersCount)
.arg(scannedSelfHosted->containers.size())
.toUtf8().constData());
verifyAdminAccess(serverIndex);
qDebug() << "Test completed successfully. Server has admin access and all containers are initialized.";
}
void testRemoveAllContainers() {
ServerCredentials credentials = getCredentialsFromEnv();
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
QSKIP("Set SELF_HOSTED_SERVER_HOST_NAME, SELF_HOSTED_SERVER_USER_NAME, SELF_HOSTED_SERVER_PASSWORD");
}
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
verifySshConnection(credentials);
int awgPort = 55424;
TransportProto awgTransportProto = TransportProto::Udp;
bool wasAwgInstalled = false;
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
ErrorCode installServerError = m_coreController->m_installController->installServer(
credentials, DockerContainer::Awg, awgPort, awgTransportProto, wasAwgInstalled);
QVERIFY2(installServerError == ErrorCode::NoError,
QString("installServer for Awg should succeed. Error: %1")
.arg(static_cast<int>(installServerError))
.toUtf8().constData());
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
qDebug() << "Server with Awg container added at index:" << serverIndex;
const auto adminBeforeRemoval = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(adminBeforeRemoval.has_value(), "Server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig *selfHostedBeforeRemoval = &(*adminBeforeRemoval);
QVERIFY2(!selfHostedBeforeRemoval->containers.isEmpty(), "Server should have containers before removal");
QVERIFY2(selfHostedBeforeRemoval->defaultContainer != DockerContainer::None, "Server should have default container before removal");
qDebug() << "Containers before removal:" << selfHostedBeforeRemoval->containers.size();
const QString removeServerId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
ErrorCode removeError = m_coreController->m_installController->removeAllContainers(removeServerId);
QVERIFY2(removeError == ErrorCode::NoError,
QString("removeAllContainers should succeed. Error: %1")
.arg(static_cast<int>(removeError))
.toUtf8().constData());
qDebug() << "All containers removed successfully";
const auto adminAfterRemoval = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(adminAfterRemoval.has_value(), "Server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig *selfHostedAfterRemoval = &(*adminAfterRemoval);
QVERIFY2(selfHostedAfterRemoval->containers.isEmpty(),
"Server should have no containers after removal");
QVERIFY2(selfHostedAfterRemoval->defaultContainer == DockerContainer::None,
"Default container should be None after removal");
qDebug() << "Containers after removal:" << selfHostedAfterRemoval->containers.size();
verifyAdminAccess(serverIndex);
qDebug() << "Test completed successfully. All containers removed and server is empty.";
}
};
QTEST_MAIN(TestSelfHostedServerSetup)
#include "testSelfHostedServerSetup.moc"
+195
View File
@@ -0,0 +1,195 @@
#include <QTest>
#include <QDebug>
#include <QFile>
#include <QSslSocket>
#include <QSslConfiguration>
#ifdef Q_OS_ANDROID
#include <QDir>
#include <QJniObject>
#include <QStandardPaths>
#include <dlfcn.h>
#include <fcntl.h>
#include <unistd.h>
#endif
// Verifies that the bundled OpenSSL is loaded on Android, not the system LibreSSL/Conscrypt.
// Uses only Qt's SSL abstraction so no direct libssl/libcrypto linkage is needed,
// which avoids bundling versioned .so.3 files that Android does not support.
class TestAndroidOpenSSL : public QObject
{
Q_OBJECT
private slots:
void testBundledLibraryLoaded()
{
const bool sslSupported = QSslSocket::supportsSsl();
qInfo() << "QSslSocket::supportsSsl() :" << sslSupported;
QVERIFY2(sslSupported,
"OpenSSL is not available – libssl.so / libcrypto.so "
"are not bundled in the APK or failed to load");
}
void testNotLibreSSL()
{
const QString runtimeVersion = QSslSocket::sslLibraryVersionString();
qInfo() << "SSL runtime version :" << runtimeVersion;
QVERIFY2(runtimeVersion.startsWith("OpenSSL "),
qPrintable(
QString("Unexpected SSL library on Android.\n"
" Got: %1\n"
" Expected prefix: 'OpenSSL '")
.arg(runtimeVersion)));
}
void testRuntimeVersionMatchesCompileTime()
{
const QString buildVersion = QSslSocket::sslLibraryBuildVersionString();
const QString runtimeVersion = QSslSocket::sslLibraryVersionString();
qInfo() << "SSL compile-time version :" << buildVersion;
qInfo() << "SSL runtime version :" << runtimeVersion;
qInfo() << "Runtime == Compile :" << (runtimeVersion == buildVersion ? "YES" : "NO");
QVERIFY2(runtimeVersion.startsWith("OpenSSL "),
"Runtime SSL library is not OpenSSL – wrong .so bundled in APK");
}
void testQtUsesOurOpenSSL()
{
const QString runtimeVersion = QSslSocket::sslLibraryVersionString();
const QString buildVersion = QSslSocket::sslLibraryBuildVersionString();
qInfo() << "Qt SSL runtime version :" << runtimeVersion;
qInfo() << "Qt SSL build version :" << buildVersion;
qInfo() << "Runtime == Build :" << (runtimeVersion == buildVersion ? "YES" : "NO");
// The bundled conan OpenSSL must be loaded (not the system Conscrypt)
QVERIFY2(runtimeVersion.startsWith("OpenSSL "),
qPrintable(
QString("Qt is not using our bundled OpenSSL.\n"
" Runtime : %1")
.arg(runtimeVersion)));
}
#ifdef Q_OS_ANDROID
// Verifies that libcrypto.so was loaded from the app's own APK bundle,
// not from /system/lib or /apex. Uses dladdr() on a well-known OpenSSL
// symbol to get the exact path of the mapped shared library.
void testLibcryptoLoadedFromAppBundle()
{
QVERIFY2(QSslSocket::supportsSsl(), "OpenSSL not loaded – cannot check bundle path");
// dlopen with RTLD_NOLOAD returns a handle to the already-loaded library
// without incrementing its reference count (no new load occurs).
void *handle = ::dlopen("libcrypto.so", RTLD_NOLOAD | RTLD_LAZY);
if (!handle) {
// Some builds use the versioned name
handle = ::dlopen("libcrypto_3.so", RTLD_NOLOAD | RTLD_LAZY);
}
QVERIFY2(handle != nullptr,
qPrintable(QString("libcrypto.so not found via dlopen(RTLD_NOLOAD): %1")
.arg(::dlerror())));
// Resolve a well-known symbol and ask dladdr for the library path
const void *sym = ::dlsym(handle, "OpenSSL_version");
::dlclose(handle);
QVERIFY2(sym != nullptr, "OpenSSL_version symbol not found in libcrypto");
Dl_info info{};
QVERIFY2(::dladdr(sym, &info) && info.dli_fname,
"dladdr failed to resolve libcrypto path");
const QString cryptoPath = QString::fromLocal8Bit(info.dli_fname);
qInfo() << "libcrypto.so loaded from:" << cryptoPath;
const bool fromSystem = cryptoPath.startsWith(QLatin1String("/system/"))
|| cryptoPath.startsWith(QLatin1String("/apex/"));
QVERIFY2(!fromSystem,
qPrintable(QString("System libcrypto.so loaded instead of bundled!\n"
" Path: %1")
.arg(cryptoPath)));
const bool fromApp = cryptoPath.startsWith(QLatin1String("/data/app/"))
|| cryptoPath.contains(
QLatin1String("org.qtproject.example.test_android_openssl"));
QVERIFY2(fromApp,
qPrintable(QString("libcrypto.so not from app bundle.\n"
" Path : %1\n"
" Expect: /data/app/...")
.arg(cryptoPath)));
}
#endif
};
#ifdef Q_OS_ANDROID
// On Android, androidtestrunner reads test output from files/stdout.txt.
// It starts `tail -f files/stdout.txt` immediately after launching the test,
// and KILLS the process if the file doesn't exist yet (exit=255).
//
// Fix: pre-create files/stdout.txt via JNI before QCoreApplication initializes
// (JNI is already set up by the Qt Activity host before main() is called).
// This gives androidtestrunner's tail something to attach to right away.
int main(int argc, char *argv[])
{
// Pre-create stdout.txt via JNI so androidtestrunner can tail it immediately.
QString filesDir;
{
QJniObject activity = QJniObject::callStaticObjectMethod(
"org/qtproject/qt/android/QtNative", "activity",
"()Landroid/app/Activity;");
if (activity.isValid()) {
QJniObject dir = activity.callObjectMethod(
"getFilesDir", "()Ljava/io/File;");
if (dir.isValid())
filesDir = dir.callObjectMethod<jstring>("getAbsolutePath").toString();
}
}
if (!filesDir.isEmpty()) {
const QByteArray path = (filesDir + "/stdout.txt").toLocal8Bit();
int fd = ::open(path.constData(), O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd >= 0) ::close(fd);
}
QCoreApplication app(argc, argv);
app.setAttribute(Qt::AA_Use96Dpi, true);
if (filesDir.isEmpty())
filesDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir().mkpath(filesDir);
const QString stdoutPath = filesDir + "/stdout.txt";
// Build a clean argument list from scratch with the absolute output path.
// Do NOT parse QCoreApplication::arguments() — on Android these include
// Intent extras that may already contain a relative "-o stdout.txt,txt",
// and attempting to patch them causes Qt to misinterpret the path as a
// test function name.
QStringList args;
args << QCoreApplication::applicationFilePath()
<< QStringLiteral("-o") << (stdoutPath + QStringLiteral(",txt"))
<< QStringLiteral("-v2");
TestAndroidOpenSSL tc;
const int result = QTest::qExec(&tc, args);
// Copy to Documents for easy manual inspection via the Files app.
const QString docsDir =
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
if (!docsDir.isEmpty()) {
QDir().mkpath(docsDir);
const QString dst = docsDir + "/test_android_openssl_results.txt";
QFile::remove(dst);
QFile::copy(stdoutPath, dst);
}
return result;
}
#else
QTEST_GUILESS_MAIN(TestAndroidOpenSSL)
#endif
#include "testAndroidOpenSSL.moc"
+152
View File
@@ -0,0 +1,152 @@
#include <QTest>
#include <QDebug>
#include <QSignalSpy>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include "utils/testCoreController.h"
#include "utils/testUtils.h"
#include "core/controllers/settingsController.h"
#include "core/repositories/secureServersRepository.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestAndroidRestoreBackup : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
void importServer(const QString &key)
{
auto result = m_coreController->m_importCoreController->extractConfigFromData(key);
QVERIFY2(result.errorCode == ErrorCode::NoError, "Server import must succeed");
m_coreController->m_importCoreController->importConfig(result.config);
}
private slots:
void initTestCase()
{
const 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();
m_coreController->m_serversRepository->invalidateCache();
}
// -----------------------------------------------------------------------
// Android safety: killSwitch and autoConnect must be DISABLED after restore
// even if the backup was created with them enabled.
// (SettingsController::restoreAppConfigFromData unconditionally resets
// these flags on Android / iOS – see Q_OS_ANDROID block in the impl.)
// -----------------------------------------------------------------------
void testRestoreDisablesDangerousSettingsOnAndroid()
{
const QString key = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
if (!isEnvValueConfigured(key)) {
QSKIP("THIRD_PARTY_AWG_VPN_KEY not set – skipping");
}
importServer(key);
// Enable dangerous settings before creating the backup
m_coreController->m_appSettingsRepository->setKillSwitchEnabled(true);
m_coreController->m_appSettingsRepository->setAutoConnect(true);
QVERIFY(m_coreController->m_appSettingsRepository->isKillSwitchEnabled());
QVERIFY(m_coreController->m_appSettingsRepository->isAutoConnect());
const QByteArray backup = m_coreController->m_settingsController->backupAppConfig();
QVERIFY(!backup.isEmpty());
const QJsonObject backupJson = QJsonDocument::fromJson(backup).object();
qInfo() << "Backup killSwitchEnabled :" << backupJson.value("Conf/killSwitchEnabled").toBool();
qInfo() << "Backup autoConnect :" << backupJson.value("Conf/autoConnect").toBool();
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
const ErrorCode err = m_coreController->m_settingsController->restoreAppConfigFromData(backup);
QVERIFY2(err == ErrorCode::NoError, "Restore must succeed");
const bool killSwitch = m_coreController->m_appSettingsRepository->isKillSwitchEnabled();
const bool autoConnect = m_coreController->m_appSettingsRepository->isAutoConnect();
qInfo() << "killSwitchEnabled after restore:" << killSwitch;
qInfo() << "autoConnect after restore :" << autoConnect;
QVERIFY2(!killSwitch,
"killSwitch must be DISABLED after restore on Android (safety reset)");
QVERIFY2(!autoConnect,
"autoConnect must be DISABLED after restore on Android (safety reset)");
}
// -----------------------------------------------------------------------
// Cross-platform backup: restoring a backup from another platform must
// succeed and must clear the app split-tunneling apps list on Android.
// -----------------------------------------------------------------------
void testRestoreFromOtherPlatformClearsAppSplitTunnelingList()
{
const QString key = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
if (!isEnvValueConfigured(key)) {
QSKIP("THIRD_PARTY_AWG_VPN_KEY not set – skipping");
}
importServer(key);
// Build a synthetic backup that claims to come from Windows
QByteArray baseBackup = m_coreController->m_settingsController->backupAppConfig();
QJsonObject obj = QJsonDocument::fromJson(baseBackup).object();
obj["AppPlatform"] = "Windows";
const QByteArray crossPlatformBackup = QJsonDocument(obj).toJson();
QSignalSpy clearAppsListSpy(m_coreController->m_settingsController,
&SettingsController::appSplitTunnelingClearAppsList);
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
const ErrorCode err =
m_coreController->m_settingsController->restoreAppConfigFromData(crossPlatformBackup);
QVERIFY2(err == ErrorCode::NoError, "Cross-platform restore must succeed");
qInfo() << "appSplitTunnelingClearAppsList emitted:" << clearAppsListSpy.count() << "time(s)";
QVERIFY2(clearAppsListSpy.count() == 1,
"appSplitTunnelingClearAppsList must be emitted when restoring from different platform");
}
// -----------------------------------------------------------------------
// Invalid data must return an error, not crash
// -----------------------------------------------------------------------
void testRestoreInvalidDataReturnsError()
{
const QByteArray garbage = QByteArray("not a valid backup {{{{");
const ErrorCode err = m_coreController->m_settingsController->restoreAppConfigFromData(garbage);
qInfo() << "ErrorCode for invalid backup:" << static_cast<int>(err);
QVERIFY2(err != ErrorCode::NoError,
"Restoring invalid data must return an error code");
}
};
QTEST_MAIN(TestAndroidRestoreBackup)
#include "testAndroidRestoreBackup.moc"
+108
View File
@@ -0,0 +1,108 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QFile>
#include <QFileInfo>
#include <QDebug>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia::test;
class TestBackupExportImport : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
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();
}
void testBackupExportImport() {
QString backup = getEnvValue("BACKUP_PATH");
logEnvValueState("BACKUP_PATH");
qInfo() << "BACKUP_PATH file exists:" << QFile::exists(backup)
<< "size:" << QFileInfo(backup).size();
if (!isEnvValueConfigured(backup)) {
QSKIP("Set BACKUP_PATH");
}
QSignalSpy errorOccurredSpy(m_coreController->m_settingsUiController, &SettingsUiController::errorOccurred);
m_coreController->m_settingsUiController->restoreAppConfig(backup);
QVERIFY2(errorOccurredSpy.count() == 0, "errorOccurred signal should NOT be emitted (default is already 0)");
QByteArray backupData = m_coreController->m_settingsController->backupAppConfig();
QFile file(backup);
QVERIFY2(file.open(QIODevice::ReadOnly), "Backup file should open for reading");
const QByteArray fileData = file.readAll();
QJsonObject importedObj = QJsonDocument::fromJson(fileData).object();
QJsonObject exportedObj = QJsonDocument::fromJson(backupData).object();
QVERIFY2(!importedObj.isEmpty(), "Backup file should contain valid JSON");
QVERIFY2(!exportedObj.isEmpty(), "Exported backup should contain valid JSON");
// backupAppConfig() injects live platform state on every export
// (AppPlatform, autoStart, killSwitch...), and restoreAppConfigFromData()
// normalizes split tunneling / startMinimized / ExceptApps to the current
// platform, so these keys legitimately differ from a backup made on
// another platform/machine.
const QStringList volatileKeys = { "AppPlatform", "Conf/autoStart", "Conf/killSwitchEnabled",
"Conf/strictKillSwitchEnabled", "Conf/useAmneziaDns",
"Conf/appsSplitTunnelingEnabled", "Conf/sitesSplitTunnelingEnabled",
"Conf/startMinimized", "Conf/ExceptApps" };
for (const QString &key : volatileKeys) {
importedObj.remove(key);
exportedObj.remove(key);
}
if (exportedObj != importedObj) {
for (auto it = importedObj.constBegin(); it != importedObj.constEnd(); ++it) {
if (!exportedObj.contains(it.key())) {
qWarning() << "Key missing in exported backup:" << it.key();
} else if (exportedObj.value(it.key()) != it.value()) {
qWarning() << "Key differs:" << it.key();
}
}
for (auto it = exportedObj.constBegin(); it != exportedObj.constEnd(); ++it) {
if (!importedObj.contains(it.key())) {
qWarning() << "Extra key in exported backup:" << it.key();
}
}
}
QVERIFY2(exportedObj == importedObj,
"Imported and exported backup config should be equal (ignoring platform-specific keys)");
}
};
QTEST_MAIN(TestBackupExportImport)
#include "testBackupExportImport.moc"
+141
View File
@@ -0,0 +1,141 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
#include "core/utils/utilities.h"
#include "version.h"
using namespace amnezia;
using namespace amnezia::test;
class TestConnection : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
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();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), "");
}
}
void testConnect() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
QString wgKey = getEnvValue("THIRD_PARTY_WIRE_GUARD_VPN_KEY");
QString ovpnKey = getEnvValue("THIRD_PARTY_OPEN_VPN_KEY");
QString ikevKey = getEnvValue("THIRD_PARTY_IKEV_VPN_KEY");
logEnvValueState("THIRD_PARTY_AWG_VPN_KEY");
logEnvValueState("THIRD_PARTY_XRAY_VPN_KEY");
logEnvValueState("THIRD_PARTY_WIRE_GUARD_VPN_KEY");
logEnvValueState("THIRD_PARTY_OPEN_VPN_KEY");
logEnvValueState("THIRD_PARTY_IKEV_VPN_KEY");
// toggleConnection() silently refuses to emit prepareConfig when the
// AmneziaVPN service process is not running (isConnectionSupported returns
// AmneziaServiceNotRunning), so this test needs an installed client.
if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true)) {
QSKIP("AmneziaVPN service is not running - connection flow cannot be tested");
}
QSignalSpy prepareConfigSpy(m_coreController->m_connectionUiController, &ConnectionUiController::prepareConfig);
auto importResult0 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
if (importResult0.errorCode == ErrorCode::NoError) {
m_coreController->m_importCoreController->importConfig(importResult0.config);
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(prepareConfigSpy.count() == 1, "prepareConfig signal should be emitted once");
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(m_coreController->m_connectionUiController->isConnected() == false, "Connection should NOT be active");
} else {
qWarning() << "Error on AWG key import";
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
if (importResult1.errorCode == ErrorCode::NoError) {
m_coreController->m_importCoreController->importConfig(importResult1.config);
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(prepareConfigSpy.count() == 3, "prepareConfig signal should be emitted three times");
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(m_coreController->m_connectionUiController->isConnected() == false, "Connection should NOT be active");
} else {
qWarning() << "Error on XRay key import";
}
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
if (importResult2.errorCode == ErrorCode::NoError) {
m_coreController->m_importCoreController->importConfig(importResult2.config);
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(prepareConfigSpy.count() == 5, "prepareConfig signal should be emitted five times");
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(m_coreController->m_connectionUiController->isConnected() == false, "Connection should NOT be active");
} else {
qWarning() << "Error on WG key import";
}
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(ovpnKey);
if (importResult3.errorCode == ErrorCode::NoError) {
m_coreController->m_importCoreController->importConfig(importResult3.config);
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(prepareConfigSpy.count() == 7, "prepareConfig signal should be emitted seven times");
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(m_coreController->m_connectionUiController->isConnected() == false, "Connection should NOT be active");
} else {
qWarning() << "Error on OpenVPN key import";
}
auto importResult4 = m_coreController->m_importCoreController->extractConfigFromData(ikevKey);
if (importResult4.errorCode == ErrorCode::NoError) {
m_coreController->m_importCoreController->importConfig(importResult4.config);
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(prepareConfigSpy.count() == 9, "prepareConfig signal should be emitted nine times");
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(m_coreController->m_connectionUiController->isConnected() == false, "Connection should NOT be active");
}
else {
qWarning() << "Error on IKEV key import";
}
}
};
QTEST_MAIN(TestConnection)
#include "testConnection.moc"
+136
View File
@@ -0,0 +1,136 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "ui/models/serversModel.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestDefaultServerChange : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
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();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testSetDefaultServerIndex() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
QString wgKey = getEnvValue("THIRD_PARTY_WIRE_GUARD_VPN_KEY");
if (!isEnvValueConfigured(awgKey) || !isEnvValueConfigured(xrayKey) || !isEnvValueConfigured(wgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY, THIRD_PARTY_XRAY_VPN_KEY, THIRD_PARTY_WIRE_GUARD_VPN_KEY");
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
m_coreController->m_importCoreController->importConfig(importResult3.config);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted");
QVERIFY2(defaultServerChangedSpy.at(0).at(0).toString() == m_coreController->m_serversController->getServerId(0),
"defaultServerChanged should emit new default server id");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0");
if (m_coreController->m_serversModel) {
int modelDefaultIndex = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool() ? 0 : -1;
QVERIFY2(modelDefaultIndex == 0, "Model should reflect default server");
}
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(2));
QVERIFY2(defaultServerChangedSpy.count() == 2, "defaultServerChanged signal should be emitted again");
QVERIFY2(defaultServerChangedSpy.at(1).at(0).toString() == m_coreController->m_serversController->getServerId(2),
"defaultServerChanged should emit new default server id");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default server index should be 2");
}
void testDefaultServerChangeOnRemoveEdgeCases() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
QString wgKey = getEnvValue("THIRD_PARTY_WIRE_GUARD_VPN_KEY");
if (!isEnvValueConfigured(awgKey) || !isEnvValueConfigured(xrayKey) || !isEnvValueConfigured(wgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY, THIRD_PARTY_XRAY_VPN_KEY, THIRD_PARTY_WIRE_GUARD_VPN_KEY");
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
m_coreController->m_importCoreController->importConfig(importResult3.config);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Should have 2 servers");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1 (was 2, removed 0)");
const auto description1 = serverDescriptionAt(m_coreController->m_serversRepository, 0);
const auto description2 = serverDescriptionAt(m_coreController->m_serversRepository, 1);
QVERIFY2(description1.has_value() && description2.has_value(), "Server configs should exist");
QVERIFY2(*description1 == "Xray Server", "First remaining server should be Xray");
QVERIFY2(*description2 == "WireGuard Server", "Second remaining server should be WireGuard");
defaultServerChangedSpy.clear();
serverRemovedSpy.clear();
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have 1 server");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default should be index 0 (was 1, removed 0)");
const auto lastDescription = serverDescriptionAt(m_coreController->m_serversRepository, 0);
QVERIFY2(lastDescription.has_value(), "Server config should exist");
QVERIFY2(*lastDescription == "WireGuard Server", "Last server should be WireGuard");
}
};
QTEST_MAIN(TestDefaultServerChange)
#include "testDefaultServerChange.moc"
+224
View File
@@ -0,0 +1,224 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "ui/models/serversModel.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestMultipleImports : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
void serverImportTest(QSignalSpy &importFinishedSpy, QSignalSpy &defaultServerChangedSpy, const QString &key, const int &expected, const QString &expectedDescription) {
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(key);
QVERIFY2(importResult.errorCode == ErrorCode::NoError, qPrintable(QString("Import should succeed, expected times: %1").arg(expected)));
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(importFinishedSpy.count() == expected, qPrintable(QString("importFinished signal should be emitted times: %1").arg(expected)));
QVERIFY2(defaultServerChangedSpy.count() == expected-1, qPrintable(QString("defaultServerChanged signal should be emitted times: %1").arg(expected-1)));
QVERIFY2(m_coreController->m_serversRepository->serversCount() == expected, qPrintable(QString("After import servers count should be: %1").arg(expected)));
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == expected, qPrintable(QString("After import model row count should be: %1").arg(expected)));
}
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == expected-1, qPrintable(QString("Default server index should be: %1").arg(expected-1)));
const auto description = serverDescriptionAt(m_coreController->m_serversRepository, expected-1);
QVERIFY2(description.has_value(), "Server config should exist");
if (*description == expectedDescription) qWarning() << "Server description should match";
if (m_coreController->m_serversModel) {
QString modelDesc = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(expected-1, 0), ServersModel::NameRole).toString();
if (modelDesc == expectedDescription) qWarning() << "Server description in model should match";
}
}
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();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testMultipleImports() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
QString wgKey = getEnvValue("THIRD_PARTY_WIRE_GUARD_VPN_KEY");
QString ovpnKey = getEnvValue("THIRD_PARTY_OPEN_VPN_KEY");
QString cloakKey = getEnvValue("THIRD_PARTY_CLOAK_VPN_KEY");
QString ssKey = getEnvValue("THIRD_PARTY_SS_VPN_KEY");
QString premKey = getEnvValue("THIRD_PARTY_PREMIUM_VPN_KEY");
logEnvValueState("THIRD_PARTY_AWG_VPN_KEY");
logEnvValueState("THIRD_PARTY_XRAY_VPN_KEY");
logEnvValueState("THIRD_PARTY_WIRE_GUARD_VPN_KEY");
logEnvValueState("THIRD_PARTY_OPEN_VPN_KEY");
logEnvValueState("THIRD_PARTY_CLOAK_VPN_KEY");
logEnvValueState("THIRD_PARTY_SS_VPN_KEY");
logEnvValueState("THIRD_PARTY_PREMIUM_VPN_KEY");
if (!isEnvValueConfigured(awgKey) || !isEnvValueConfigured(xrayKey) || !isEnvValueConfigured(wgKey)
|| !isEnvValueConfigured(ovpnKey) || !isEnvValueConfigured(cloakKey) || !isEnvValueConfigured(ssKey)
|| !isEnvValueConfigured(premKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY, THIRD_PARTY_XRAY_VPN_KEY, THIRD_PARTY_WIRE_GUARD_VPN_KEY, "
"THIRD_PARTY_OPEN_VPN_KEY, THIRD_PARTY_CLOAK_VPN_KEY, THIRD_PARTY_SS_VPN_KEY, "
"THIRD_PARTY_PREMIUM_VPN_KEY");
}
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Initial servers count should be 0");
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Initial model row count should be 0");
}
serverImportTest(importFinishedSpy, defaultServerChangedSpy, awgKey, 1, "AWG Server");
serverImportTest(importFinishedSpy, defaultServerChangedSpy, xrayKey, 2, "Xray Server");
serverImportTest(importFinishedSpy, defaultServerChangedSpy, wgKey, 3, "WireGuard Server");
serverImportTest(importFinishedSpy, defaultServerChangedSpy, ovpnKey, 4, "OpenVPN Server");
serverImportTest(importFinishedSpy, defaultServerChangedSpy, cloakKey, 5, "Cloak Server");
serverImportTest(importFinishedSpy, defaultServerChangedSpy, ssKey, 6, "ShadowSocks Server");
serverImportTest(importFinishedSpy, defaultServerChangedSpy, premKey, 7, "Amnezia Premium");
}
void testMultipleImportsRemoval() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
if (!isEnvValueConfigured(awgKey) || !isEnvValueConfigured(xrayKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY and THIRD_PARTY_XRAY_VPN_KEY");
}
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Initial servers count should be 0");
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
QVERIFY2(importResult1.errorCode == ErrorCode::NoError, "First import should succeed");
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
QVERIFY2(importResult2.errorCode == ErrorCode::NoError, "Second import should succeed");
m_coreController->m_importCoreController->importConfig(importResult2.config);
QVERIFY2(importFinishedSpy.count() == 2, "importFinished signal should be emitted twice");
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted once (0->1, first import doesn't emit)");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "After two imports servers count should be 2");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Second server should be default");
const auto description0 = serverDescriptionAt(m_coreController->m_serversRepository, 0);
const auto description1 = serverDescriptionAt(m_coreController->m_serversRepository, 1);
QVERIFY2(description0.has_value() && description1.has_value(), "Server configs should exist");
if (*description0 == "AWG Server") qWarning() << "First server description should match";
if (*description1 == "Xray Server") qWarning() << "Second server description should match";
defaultServerChangedSpy.clear();
serverRemovedSpy.clear();
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(serverRemovedSpy.at(0).at(1).toInt() == 0, "serverRemoved should emit removed index 0");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "After removing first server, servers count should be 1");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "After removing first server, default index should be 0");
const auto remainingDescription = serverDescriptionAt(m_coreController->m_serversRepository, 0);
QVERIFY2(remainingDescription.has_value(), "Server config should exist");
if (*remainingDescription == "Xray Server") qWarning() << "Remaining server should be Xray Server";
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "After removing first server, model row count should be 1");
QString modelDesc = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
if (modelDesc == "Xray Server") qWarning() << "Remaining server description in model should match";
}
defaultServerChangedSpy.clear();
serverRemovedSpy.clear();
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(serverRemovedSpy.at(0).at(1).toInt() == 0, "serverRemoved should emit removed index 0");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "After removing last server, servers count should be 0");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "After removing last server, default index should be 0");
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "After removing last server, model row count should be 0");
}
}
void testListImports() {
QString dnsListPath = getEnvValue("DNS_LIST_PATH");
QString ipListPath = getEnvValue("IP_LIST_PATH");
logEnvValueState("DNS_LIST_PATH");
logEnvValueState("IP_LIST_PATH");
qInfo() << "DNS_LIST_PATH file exists:" << QFile::exists(dnsListPath)
<< "size:" << QFileInfo(dnsListPath).size();
qInfo() << "IP_LIST_PATH file exists:" << QFile::exists(ipListPath)
<< "size:" << QFileInfo(ipListPath).size();
if (!isEnvValueConfigured(dnsListPath) || !isEnvValueConfigured(ipListPath)) {
QSKIP("set DNS_LIST_PATH and IP_LIST_PATH");
}
QSignalSpy dnsErrorOccurredSpy(m_coreController->m_allowedDnsUiController, &AllowedDnsUiController::errorOccurred);
QSignalSpy dnsFinishedSpy(m_coreController->m_allowedDnsUiController, &AllowedDnsUiController::finished);
QSignalSpy ipErrorOccurredSpy(m_coreController->m_ipSplitTunnelingUiController, &IpSplitTunnelingUiController::errorOccurred);
QSignalSpy ipFinishedSpy(m_coreController->m_ipSplitTunnelingUiController, &IpSplitTunnelingUiController::finished);
m_coreController->m_allowedDnsUiController->importDns(dnsListPath, true);
if (dnsErrorOccurredSpy.count() > 0) {
qWarning() << "(dns) errorOccurred:" << dnsErrorOccurredSpy.at(0).at(0).toString();
}
QVERIFY2(dnsErrorOccurredSpy.count() == 0, "(dns) errorOccurred signal should NOT be emitted");
QVERIFY2(dnsFinishedSpy.count() == 1, "(dns) finished signal should be emitted");
m_coreController->m_ipSplitTunnelingUiController->importSites(ipListPath, true);
if (ipErrorOccurredSpy.count() > 0) {
qWarning() << "(ip) errorOccurred:" << ipErrorOccurredSpy.at(0).at(0).toString();
}
QVERIFY2(ipErrorOccurredSpy.count() == 0, "(ip) errorOccurred signal should NOT be emitted");
QVERIFY2(ipFinishedSpy.count() == 1, "(ip) finished signal should be emitted");
}
};
QTEST_MAIN(TestMultipleImports)
#include "testMultipleImports.moc"
+93
View File
@@ -0,0 +1,93 @@
#include <QTest>
#include <QDebug>
#include <QSslSocket>
#include <openssl/crypto.h>
#include <openssl/opensslv.h>
class TestOpenSSLLibrary : public QObject
{
Q_OBJECT
private slots:
void testRuntimeVersionMatchesCompileTime()
{
const QString compileVersion = QString::fromUtf8(OPENSSL_VERSION_TEXT);
const QString runtimeVersion = QString::fromUtf8(OpenSSL_version(OPENSSL_VERSION));
qInfo() << "OpenSSL compile-time version :" << compileVersion;
qInfo() << "OpenSSL runtime version :" << runtimeVersion;
// If the system library (e.g. macOS LibreSSL) was loaded instead of ours,
// the runtime string will differ from the compile-time macro.
QVERIFY2(runtimeVersion == compileVersion,
qPrintable(
QString("OpenSSL version mismatch – system library loaded instead of bundled one.\n"
" Compile-time : %1\n"
" Runtime : %2")
.arg(compileVersion, runtimeVersion)));
}
void testNotSystemLibreSSL()
{
const QString runtimeVersion = QString::fromUtf8(OpenSSL_version(OPENSSL_VERSION));
qInfo() << "OpenSSL runtime version:" << runtimeVersion;
QVERIFY2(runtimeVersion.startsWith("OpenSSL "),
qPrintable(
QString("System library loaded instead of bundled OpenSSL.\n"
" Got: %1\n"
" Expected prefix: 'OpenSSL '")
.arg(runtimeVersion)));
}
void testQtUsesOurOpenSSL()
{
const QString qtRuntimeVersion = QSslSocket::sslLibraryVersionString();
const QString qtBuildVersion = QSslSocket::sslLibraryBuildVersionString();
const QString ourRuntime = QString::fromUtf8(OpenSSL_version(OPENSSL_VERSION));
qInfo() << "Qt SSL runtime version :" << qtRuntimeVersion;
qInfo() << "Qt SSL build version :" << qtBuildVersion;
qInfo() << "App OpenSSL runtime :" << ourRuntime;
qInfo() << "Qt runtime == App runtime:" << (qtRuntimeVersion == ourRuntime ? "YES" : "NO");
QVERIFY2(qtRuntimeVersion.startsWith("OpenSSL "),
qPrintable("Qt is not using OpenSSL at runtime, got: " + qtRuntimeVersion));
#ifndef Q_OS_WIN
QVERIFY2(qtRuntimeVersion == ourRuntime,
qPrintable(
QString("Qt SSL library differs from the OpenSSL linked by the app.\n"
" Qt runtime : %1\n"
" App runtime : %2")
.arg(qtRuntimeVersion, ourRuntime)));
#endif
}
#ifdef Q_OS_WIN
void testWindowsNoDllHijack()
{
const QString runtimeVersion = QString::fromUtf8(OpenSSL_version(OPENSSL_VERSION));
const QString compileVersion = QString::fromUtf8(OPENSSL_VERSION_TEXT);
const QString opensslDir = QString::fromUtf8(OpenSSL_version(OPENSSL_DIR));
qInfo() << "Windows OpenSSL runtime version :" << runtimeVersion;
qInfo() << "Windows OpenSSL compile version :" << compileVersion;
qInfo() << "Windows OpenSSL configured dir :" << opensslDir;
qInfo() << "Runtime == Compile :" << (runtimeVersion == compileVersion ? "YES" : "NO");
QVERIFY2(runtimeVersion == compileVersion,
qPrintable(
QString("Wrong OpenSSL DLL loaded on Windows (possible PATH hijack).\n"
" Expected (compile-time) : %1\n"
" Got (runtime) : %2\n"
" OpenSSL configured dir : %3")
.arg(compileVersion, runtimeVersion, opensslDir)));
}
#endif // Q_OS_WIN
};
QTEST_MAIN(TestOpenSSLLibrary)
#include "testOpenSSLLibrary.moc"
+121
View File
@@ -0,0 +1,121 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/repositories/secureServersRepository.h"
#include "core/models/serverDescription.h"
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
#include "core/utils/serverConfigUtils.h"
using namespace amnezia;
using namespace amnezia::test;
class TestServerEdgeCases : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
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();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testInvalidIndexOperations() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
if (!isEnvValueConfigured(awgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY");
}
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have 1 server");
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(-1));
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(10));
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(100));
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server count should remain 1");
const QString validServerId = m_coreController->m_serversController->getServerId(0);
const serverConfigUtils::ConfigType editKind =
m_coreController->m_serversRepository->serverKind(validServerId);
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(-1),
QJsonObject(), editKind);
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for invalid index");
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(10),
QJsonObject(), editKind);
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for invalid index");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(-1));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for invalid index");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(10));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for invalid index");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should remain 0");
}
void testEmptyRepositoryOperations() {
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Should start with 0 servers");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for empty repository");
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(0),
SelfHostedAdminServerConfig {}.toJson(),
serverConfigUtils::ConfigType::SelfHostedAdmin);
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for empty repository");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for empty repository");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0 for empty repository");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Server count should remain 0");
}
};
QTEST_MAIN(TestServerEdgeCases)
#include "testServerEdgeCases.moc"
+113
View File
@@ -0,0 +1,113 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "ui/models/serversModel.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestServerEdit : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
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();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testServerEditTriggersHandlers() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
if (!isEnvValueConfigured(awgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY");
}
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(importFinishedSpy.count() == 1, "Import should succeed");
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
QVERIFY(m_coreController->m_serversController->renameServer(
m_coreController->m_serversController->getServerId(0), QStringLiteral("Edited AWG Server")));
QVERIFY2(serverEditedSpy.count() == 1, "serverEdited signal should be emitted");
QVERIFY2(serverEditedSpy.at(0).at(0).toString() == m_coreController->m_serversRepository->serverIdAt(0),
"serverEdited should emit edited server id");
const auto editedDescription = serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(0));
QVERIFY2(editedDescription.has_value(), "Server config should exist");
QVERIFY2(*editedDescription == "Edited AWG Server", "Server description should be updated");
if (m_coreController->m_serversModel) {
QString modelDesc = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc == "Edited AWG Server", "Server description in model should be updated");
}
}
void testServerEditPreservesDefault() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
if (!isEnvValueConfigured(awgKey) || !isEnvValueConfigured(xrayKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY and THIRD_PARTY_XRAY_VPN_KEY");
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server should be index 1");
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
QVERIFY(m_coreController->m_serversController->renameServer(
m_coreController->m_serversController->getServerId(1), QStringLiteral("Edited Default Server")));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted when editing default server");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server index should remain 1");
QVERIFY(m_coreController->m_serversController->renameServer(
m_coreController->m_serversController->getServerId(0), QStringLiteral("Edited Non-Default Server")));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted when editing non-default server");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server index should remain 1");
}
};
QTEST_MAIN(TestServerEdit)
#include "testServerEdit.moc"
+100
View File
@@ -0,0 +1,100 @@
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "ui/models/serversModel.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestServerRename : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
void importSingleServer()
{
const QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
const auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
QVERIFY2(importResult.errorCode == ErrorCode::NoError, "Import setup should succeed");
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have one server");
}
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();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testRenameServerUpdatesRepositoryAndModel()
{
if (!isEnvValueConfigured(getEnvValue("THIRD_PARTY_AWG_VPN_KEY"))) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY");
}
importSingleServer();
const QString serverId = m_coreController->m_serversController->getServerId(0);
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
QVERIFY(m_coreController->m_serversController->renameServer(serverId, QStringLiteral("Renamed Server")));
QVERIFY2(serverEditedSpy.count() == 1, "serverEdited should be emitted");
const auto renamedDescription = serverDescription(m_coreController->m_serversRepository, serverId);
QVERIFY2(renamedDescription.has_value(), "Server config should exist");
QVERIFY2(*renamedDescription == "Renamed Server", "Repository should store renamed description");
if (m_coreController->m_serversModel) {
const QString modelName = m_coreController->m_serversModel
->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole)
.toString();
QVERIFY2(modelName == "Renamed Server", "Model should reflect renamed server");
}
QVERIFY(m_coreController->m_serversController->renameServer(serverId, QStringLiteral("Renamed Again")));
QVERIFY2(serverEditedSpy.count() == 2, "serverEdited should be emitted again");
const auto renamedAgainDescription = serverDescription(m_coreController->m_serversRepository, serverId);
QVERIFY2(renamedAgainDescription.has_value(), "Server config should exist");
QVERIFY2(*renamedAgainDescription == "Renamed Again", "Repository should store second rename");
if (m_coreController->m_serversModel) {
const QString modelName = m_coreController->m_serversModel
->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole)
.toString();
QVERIFY2(modelName == "Renamed Again", "Model should reflect second rename");
}
}
};
QTEST_MAIN(TestServerRename)
#include "testServerRename.moc"
+120
View File
@@ -0,0 +1,120 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "ui/models/serversModel.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestServersModelSync : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
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 testServersModelSyncOnOperations() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
if (!m_coreController->m_serversModel) {
QSKIP("ServersModel not available");
}
if (!isEnvValueConfigured(awgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY");
}
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Initial model row count should be 0");
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "Model should have 1 row after import");
QString modelDesc1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc1 == "AWG Server", "Model should have correct server name");
QVERIFY(m_coreController->m_serversController->renameServer(
m_coreController->m_serversController->getServerId(0), QStringLiteral("Edited AWG Server")));
QString modelDesc2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc2 == "Edited AWG Server", "Model should be updated after edit");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Model should have 0 rows after removal");
}
void testServersModelDefaultIndexSync() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
QString wgKey = getEnvValue("THIRD_PARTY_WIRE_GUARD_VPN_KEY");
if (!m_coreController->m_serversModel) {
QSKIP("ServersModel not available");
}
if (!isEnvValueConfigured(awgKey) || !isEnvValueConfigured(xrayKey) || !isEnvValueConfigured(wgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY, THIRD_PARTY_XRAY_VPN_KEY, THIRD_PARTY_WIRE_GUARD_VPN_KEY");
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
m_coreController->m_importCoreController->importConfig(importResult3.config);
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
QVERIFY2(m_coreController->m_serversModel->rowCount() == 3, "Model should have 3 rows");
bool isDefault0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool();
bool isDefault1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(1, 0), ServersModel::IsDefaultRole).toBool();
bool isDefault2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::IsDefaultRole).toBool();
QVERIFY2(!isDefault0, "Server 0 should not be default");
QVERIFY2(!isDefault1, "Server 1 should not be default");
QVERIFY2(isDefault2, "Server 2 should be default");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
isDefault0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool();
isDefault2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::IsDefaultRole).toBool();
QVERIFY2(isDefault0, "Server 0 should be default after change");
QVERIFY2(!isDefault2, "Server 2 should not be default after change");
}
};
QTEST_MAIN(TestServersModelSync)
#include "testServersModelSync.moc"
+267
View File
@@ -0,0 +1,267 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QLocale>
#include <QTest>
#include "utils/testCoreController.h"
#include "ui/controllers/settingsUiController.h"
#include "ui/controllers/languageUiController.h"
#include "ui/models/allowedDnsModel.h"
#include "ui/models/ipSplitTunnelingModel.h"
#include "ui/models/appSplitTunnelingModel.h"
#include "ui/models/languageModel.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
class TestSettingsSignals : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
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();
}
void testDnsSettingsSignals() {
QSignalSpy primaryDnsChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::primaryDnsChanged);
QSignalSpy secondaryDnsChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::secondaryDnsChanged);
QSignalSpy allowedDnsServersChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::allowedDnsServersChanged);
QString primaryDns = "8.8.8.8";
QString secondaryDns = "8.8.4.4";
m_coreController->m_settingsUiController->setPrimaryDns(primaryDns);
QVERIFY2(primaryDnsChangedSpy.count() == 1, "primaryDnsChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->getPrimaryDns() == primaryDns, "Primary DNS should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->getPrimaryDns() == primaryDns, "Primary DNS should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->primaryDns() == primaryDns, "Primary DNS should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->setSecondaryDns(secondaryDns);
QVERIFY2(secondaryDnsChangedSpy.count() == 1, "secondaryDnsChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->getSecondaryDns() == secondaryDns, "Secondary DNS should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->getSecondaryDns() == secondaryDns, "Secondary DNS should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->secondaryDns() == secondaryDns, "Secondary DNS should be available in SecureAppSettingsRepository");
QStringList dnsList = {"1.1.1.1", "1.0.0.1"};
m_coreController->m_allowedDnsController->addDnsList(dnsList, true);
QVERIFY2(allowedDnsServersChangedSpy.count() == 1, "allowedDnsServersChanged signal should be emitted");
QVERIFY2(m_coreController->m_appSettingsRepository->getAllowedDnsServers() == dnsList, "Allowed DNS servers should be updated in SecureAppSettingsRepository");
QVERIFY2(m_coreController->m_allowedDnsController->getCurrentDnsServers() == dnsList, "Allowed DNS servers should be available in AllowedDnsController");
QVERIFY2(m_coreController->m_allowedDnsUiController != nullptr, "AllowedDnsUiController should exist");
QVERIFY2(m_coreController->m_allowedDnsModel != nullptr, "AllowedDnsModel should exist");
QStringList modelDnsList;
for (int i = 0; i < m_coreController->m_allowedDnsModel->rowCount(); ++i) {
modelDnsList.append(m_coreController->m_allowedDnsModel->data(m_coreController->m_allowedDnsModel->index(i, 0), AllowedDnsModel::IpRole).toString());
}
QVERIFY2(modelDnsList == dnsList, "Allowed DNS servers should be available in AllowedDnsModel");
}
void testAmneziaDnsToggleSignal() {
QSignalSpy amneziaDnsToggledSpy(m_coreController->m_settingsUiController, &SettingsUiController::amneziaDnsToggled);
QSignalSpy useAmneziaDnsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::useAmneziaDnsChanged);
bool initialValue = m_coreController->m_settingsController->isAmneziaDnsEnabled();
m_coreController->m_settingsUiController->toggleAmneziaDns(!initialValue);
QVERIFY2(amneziaDnsToggledSpy.count() == 1, "amneziaDnsToggled signal should be emitted");
QVERIFY2(amneziaDnsToggledSpy.at(0).at(0).toBool() == !initialValue, "amneziaDnsToggled should emit correct value");
QVERIFY2(useAmneziaDnsChangedSpy.count() == 1, "useAmneziaDnsChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->isAmneziaDnsEnabled() == !initialValue, "Amnezia DNS state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isAmneziaDnsEnabled() == !initialValue, "Amnezia DNS state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->useAmneziaDns() == !initialValue, "Amnezia DNS state should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->toggleAmneziaDns(initialValue);
QVERIFY2(amneziaDnsToggledSpy.count() == 2, "amneziaDnsToggled signal should be emitted again");
QVERIFY2(useAmneziaDnsChangedSpy.count() == 2, "useAmneziaDnsChanged signal should be emitted again");
QVERIFY2(m_coreController->m_settingsUiController->isAmneziaDnsEnabled() == initialValue, "Amnezia DNS state should be restored in SettingsUiController");
}
void testLoggingSignals() {
QSignalSpy loggingStateChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::loggingStateChanged);
QSignalSpy saveLogsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::saveLogsChanged);
bool initialLogging = m_coreController->m_settingsController->isLoggingEnabled();
m_coreController->m_settingsUiController->toggleLogging(!initialLogging);
QVERIFY2(loggingStateChangedSpy.count() == 1, "loggingStateChanged signal should be emitted");
QVERIFY2(saveLogsChangedSpy.count() == 1, "saveLogsChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->isLoggingEnabled() == !initialLogging, "Logging state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isLoggingEnabled() == !initialLogging, "Logging state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isSaveLogs() == !initialLogging, "Logging state should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->toggleLogging(initialLogging);
QVERIFY2(loggingStateChangedSpy.count() == 2, "loggingStateChanged signal should be emitted again");
QVERIFY2(saveLogsChangedSpy.count() == 2, "saveLogsChanged signal should be emitted again");
QVERIFY2(m_coreController->m_settingsUiController->isLoggingEnabled() == initialLogging, "Logging state should be restored in SettingsUiController");
}
void testScreenshotsSignals() {
QSignalSpy screenshotsEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged);
bool initialScreenshots = m_coreController->m_settingsController->isScreenshotsEnabled();
m_coreController->m_settingsUiController->toggleScreenshotsEnabled(!initialScreenshots);
QVERIFY2(screenshotsEnabledChangedSpy.count() == 1, "screenshotsEnabledChanged signal should be emitted");
QVERIFY2(screenshotsEnabledChangedSpy.at(0).at(0).toBool() == !initialScreenshots, "screenshotsEnabledChanged should emit correct value");
QVERIFY2(m_coreController->m_settingsController->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be available in SecureAppSettingsRepository");
}
void testStartMinimizedSignals() {
QSignalSpy startMinimizedChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::startMinimizedChanged);
bool initialStartMinimized = m_coreController->m_settingsController->isStartMinimizedEnabled();
// AutoStart should be enabled else isStartMinimizedEnabled will return false
m_coreController->m_settingsUiController->toggleAutoStart(true);
m_coreController->m_settingsUiController->toggleStartMinimized(!initialStartMinimized);
QVERIFY2(startMinimizedChangedSpy.count() == 2, "startMinimizedChanged signal should be emitted 2 times: on autoStart and StartMinimized");
QVERIFY2(m_coreController->m_settingsController->isStartMinimizedEnabled() == !initialStartMinimized, "Start minimized state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isStartMinimizedEnabled() == !initialStartMinimized, "Start minimized state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isStartMinimized() == !initialStartMinimized, "Start minimized state should be available in SecureAppSettingsRepository");
}
void testAutoConnectSignals() {
bool initialAutoConnect = m_coreController->m_settingsController->isAutoConnectEnabled();
m_coreController->m_settingsUiController->toggleAutoConnect(!initialAutoConnect);
QVERIFY2(m_coreController->m_settingsController->isAutoConnectEnabled() == !initialAutoConnect, "Auto connect state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isAutoConnectEnabled() == !initialAutoConnect, "Auto connect state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isAutoConnect() == !initialAutoConnect, "Auto connect state should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->toggleAutoConnect(initialAutoConnect);
QVERIFY2(m_coreController->m_settingsController->isAutoConnectEnabled() == initialAutoConnect, "Auto connect state should be restored in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isAutoConnectEnabled() == initialAutoConnect, "Auto connect state should be restored in SettingsUiController");
}
void testLanguageChangeSignals() {
QSignalSpy appLanguageChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appLanguageChanged);
QSignalSpy translationsUpdatedSpy(m_coreController->m_languageUiController, &LanguageUiController::translationsUpdated);
QLocale initialLocale = m_coreController->m_settingsController->getAppLanguage();
QLocale newLocale = (initialLocale.language() == QLocale::English) ? QLocale::Russian : QLocale::English;
m_coreController->m_settingsController->setAppLanguage(newLocale);
QVERIFY2(appLanguageChangedSpy.count() == 1, "appLanguageChanged signal should be emitted");
QVERIFY2(appLanguageChangedSpy.at(0).at(0).value<QLocale>() == newLocale, "appLanguageChanged should emit correct locale");
QVERIFY2(m_coreController->m_settingsController->getAppLanguage() == newLocale, "App language should be updated in SettingsController");
QVERIFY2(m_coreController->m_appSettingsRepository->getAppLanguage() == newLocale, "App language should be available in SecureAppSettingsRepository");
if (m_coreController->m_languageModel) {
QString newLanguageName = m_coreController->m_languageUiController->getCurrentLanguageName();
QVERIFY2(!newLanguageName.isEmpty(), "Language name should be available in LanguageUiController");
}
}
void testGatewayEndpointSignals() {
QSignalSpy gatewayEndpointChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::gatewayEndpointChanged);
QSignalSpy devGatewayEnvChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::devGatewayEnvChanged);
QString initialEndpoint = m_coreController->m_settingsController->getGatewayEndpoint();
QString newEndpoint = "https://test-gateway.example.com";
m_coreController->m_settingsUiController->setGatewayEndpoint(newEndpoint);
QVERIFY2(gatewayEndpointChangedSpy.count() == 1, "gatewayEndpointChanged signal should be emitted");
QVERIFY2(gatewayEndpointChangedSpy.at(0).at(0).toString() == newEndpoint, "gatewayEndpointChanged should emit correct endpoint");
QVERIFY2(m_coreController->m_settingsController->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be available in SecureAppSettingsRepository");
bool initialDevEnv = m_coreController->m_settingsController->isDevGatewayEnv();
m_coreController->m_settingsUiController->toggleDevGatewayEnv(!initialDevEnv);
QVERIFY2(devGatewayEnvChangedSpy.count() == 1, "devGatewayEnvChanged signal should be emitted");
QVERIFY2(devGatewayEnvChangedSpy.at(0).at(0).toBool() == !initialDevEnv, "devGatewayEnvChanged should emit correct value");
QVERIFY2(m_coreController->m_settingsController->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be available in SecureAppSettingsRepository");
}
void testSettingsClearedSignal() {
QSignalSpy settingsClearedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::settingsCleared);
m_coreController->m_settingsController->clearSettings();
QVERIFY2(settingsClearedSpy.count() == 1, "settingsCleared signal should be emitted");
}
void testSplitTunnelingSignals() {
QSignalSpy siteSplitTunnelingToggledSpy(m_coreController->m_settingsController, &SettingsController::siteSplitTunnelingToggled);
QSignalSpy appSplitTunnelingToggledSpy(m_coreController->m_settingsController, &SettingsController::appSplitTunnelingToggled);
QSignalSpy sitesSplitTunnelingEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::sitesSplitTunnelingEnabledChanged);
QSignalSpy appsSplitTunnelingEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsSplitTunnelingEnabledChanged);
QSignalSpy routeModeChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::routeModeChanged);
QSignalSpy appsRouteModeChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsRouteModeChanged);
QSignalSpy sitesChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::sitesChanged);
QSignalSpy appsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsChanged);
bool initialSitesSplitTunneling = m_coreController->m_ipSplitTunnelingController->isSplitTunnelingEnabled();
m_coreController->m_ipSplitTunnelingController->toggleSplitTunneling(!initialSitesSplitTunneling);
QVERIFY2(sitesSplitTunnelingEnabledChangedSpy.count() == 1, "sitesSplitTunnelingEnabledChanged signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingController->isSplitTunnelingEnabled() == !initialSitesSplitTunneling, "Sites split tunneling should be updated in IpSplitTunnelingController");
QVERIFY2(m_coreController->m_appSettingsRepository->isSitesSplitTunnelingEnabled() == !initialSitesSplitTunneling, "Sites split tunneling should be available in SecureAppSettingsRepository");
bool initialAppsSplitTunneling = m_coreController->m_appSplitTunnelingController->isSplitTunnelingEnabled();
m_coreController->m_appSplitTunnelingController->toggleSplitTunneling(!initialAppsSplitTunneling);
QVERIFY2(appsSplitTunnelingEnabledChangedSpy.count() == 1, "appsSplitTunnelingEnabledChanged signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingController->isSplitTunnelingEnabled() == !initialAppsSplitTunneling, "Apps split tunneling should be updated in AppSplitTunnelingController");
QVERIFY2(m_coreController->m_appSettingsRepository->isAppsSplitTunnelingEnabled() == !initialAppsSplitTunneling, "Apps split tunneling should be available in SecureAppSettingsRepository");
RouteMode initialRouteMode = m_coreController->m_ipSplitTunnelingController->getRouteMode();
RouteMode newRouteMode = (initialRouteMode == RouteMode::VpnOnlyForwardSites)
? RouteMode::VpnAllExceptSites
: RouteMode::VpnOnlyForwardSites;
m_coreController->m_ipSplitTunnelingController->setRouteMode(newRouteMode);
QVERIFY2(routeModeChangedSpy.count() == 1, "routeModeChanged signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingController->getRouteMode() == newRouteMode, "Route mode should be updated in IpSplitTunnelingController");
QVERIFY2(m_coreController->m_appSettingsRepository->routeMode() == newRouteMode, "Route mode should be available in SecureAppSettingsRepository");
AppsRouteMode initialAppsRouteMode = m_coreController->m_appSplitTunnelingController->getRouteMode();
AppsRouteMode newAppsRouteMode = (initialAppsRouteMode == AppsRouteMode::VpnAllExceptApps)
? AppsRouteMode::VpnAllApps
: AppsRouteMode::VpnAllExceptApps;
m_coreController->m_appSplitTunnelingController->setRouteMode(newAppsRouteMode);
QVERIFY2(appsRouteModeChangedSpy.count() == 1, "appsRouteModeChanged signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingController->getRouteMode() == newAppsRouteMode, "Apps route mode should be updated in AppSplitTunnelingController");
QVERIFY2(m_coreController->m_appSettingsRepository->appsRouteMode() == newAppsRouteMode, "Apps route mode should be available in SecureAppSettingsRepository");
QMap<QString, QStringList> sitesMap{{"example.com", QStringList{"1.2.3.4"}}};
m_coreController->m_ipSplitTunnelingController->addSites(sitesMap, true);
QVERIFY2(sitesChangedSpy.count() >= 1, "sitesChanged signal should be emitted");
QVector<QPair<QString, QStringList>> currentSites = m_coreController->m_ipSplitTunnelingController->getCurrentSites();
QVERIFY2(currentSites.size() >= 1, "Sites should be available in IpSplitTunnelingController");
QVERIFY2(m_coreController->m_ipSplitTunnelingUiController != nullptr, "IpSplitTunnelingUiController should exist");
QVERIFY2(m_coreController->m_ipSplitTunnelingModel != nullptr, "IpSplitTunnelingModel should exist");
m_coreController->m_ipSplitTunnelingUiController->updateModel();
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() >= 1, "Sites should be available in IpSplitTunnelingModel");
QString modelUrl = m_coreController->m_ipSplitTunnelingModel->data(m_coreController->m_ipSplitTunnelingModel->index(0, 0), IpSplitTunnelingModel::UrlRole).toString();
QVERIFY2(modelUrl == "example.com", "Site URL should be available in IpSplitTunnelingModel");
}
};
QTEST_MAIN(TestSettingsSignals)
#include "testSettingsSignals.moc"
+98
View File
@@ -0,0 +1,98 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestSignalOrder : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
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();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testSignalOrderOnImport() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
if (!isEnvValueConfigured(awgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY");
}
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
QVERIFY2(serverAddedSpy.at(0).count() > 0, "serverAdded should have arguments");
}
void testSignalOrderOnRemoveDefault() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
if (!isEnvValueConfigured(awgKey) || !isEnvValueConfigured(xrayKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY and THIRD_PARTY_XRAY_VPN_KEY");
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1");
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(1));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted when removing default server");
QVERIFY2(defaultServerChangedSpy.at(0).at(0).toString() == m_coreController->m_serversRepository->defaultServerId(),
"defaultServerChanged should emit new default server id");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0");
}
};
QTEST_MAIN(TestSignalOrder)
#include "testSignalOrder.moc"
@@ -0,0 +1,107 @@
#include <QDebug>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QUuid>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
class TestUiAllowedDnsModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
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 finishedSpy(m_coreController->m_allowedDnsUiController, &AllowedDnsUiController::finished);
QSignalSpy errorOccurredSpy(m_coreController->m_allowedDnsUiController, &AllowedDnsUiController::errorOccurred);
QString ip = "188.40.167.81";
m_coreController->m_allowedDnsUiController->addDns(ip);
m_coreController->m_allowedDnsUiController->updateModel();
QVERIFY2(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QVERIFY2(finishedSpy.count() == 1, "finished signal should be emitted");
QVERIFY2(m_coreController->m_allowedDnsModel->rowCount() == 1, "AllowedDnsModel should have 1 row");
QModelIndex allowedDnsModelIndex = m_coreController->m_allowedDnsModel->index(0, 0);
QVERIFY2(allowedDnsModelIndex.isValid(), "Site model index should be valid");
auto dnsIp = m_coreController->m_allowedDnsModel->data(allowedDnsModelIndex, AllowedDnsModel::IpRole);
QString msg = QString("dns ip should be %1, got %2").arg(ip, dnsIp.toString());
QVERIFY2(dnsIp == ip, msg.toLocal8Bit().constData());
QTemporaryDir tempDir;
QVERIFY2(tempDir.isValid(), "Temporary directory should be created");
const QString dnsFile = tempDir.filePath(QStringLiteral("dns.json"));
{
QFile file(dnsFile);
QVERIFY2(file.open(QIODevice::WriteOnly), "DNS import file should be writable");
file.write(R"(["8.8.8.8", "1.1.1.1"])");
}
m_coreController->m_allowedDnsUiController->importDns(dnsFile, true);
m_coreController->m_allowedDnsUiController->updateModel();
QVERIFY2(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QVERIFY2(finishedSpy.count() == 2, "finished signal should be emitted");
QVERIFY2(m_coreController->m_allowedDnsModel->rowCount() > 1, "AllowedDnsModel should have more than 1 row");
const QString exportFile = tempDir.filePath(QStringLiteral("test_dns_export.json"));
m_coreController->m_allowedDnsUiController->exportDns(exportFile);
QVERIFY2(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QVERIFY2(finishedSpy.count() == 3, "finished signal should be emitted");
m_coreController->m_allowedDnsUiController->removeDns(0);
m_coreController->m_allowedDnsUiController->updateModel();
QVERIFY2(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QVERIFY2(finishedSpy.count() == 4, "finished signal should be emitted");
QVERIFY2(m_coreController->m_allowedDnsModel->rowCount() == 1, "AllowedDnsModel should have 1 row after removing first DNS");
m_coreController->m_allowedDnsUiController->removeDns(0);
m_coreController->m_allowedDnsUiController->updateModel();
QVERIFY2(finishedSpy.count() == 5, "finished signal should be emitted");
QVERIFY2(m_coreController->m_allowedDnsModel->rowCount() == 0, "AllowedDnsModel should have 0 rows");
}
};
QTEST_MAIN(TestUiAllowedDnsModelAndController)
#include "testUiAllowedDnsModelAndController.moc"
@@ -0,0 +1,100 @@
#include <QCoreApplication>
#include <QDebug>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QUuid>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
class TestUiAppSTModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
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 finishedSpy(m_coreController->m_appSplitTunnelingUiController, &AppSplitTunnelingUiController::finished);
QSignalSpy errorOccurredSpy(m_coreController->m_appSplitTunnelingUiController, &AppSplitTunnelingUiController::errorOccurred);
QSignalSpy isSplitTunnelingChangedSpy(m_coreController->m_appSplitTunnelingUiController, &AppSplitTunnelingUiController::isSplitTunnelingEnabledChanged);
m_coreController->m_appSplitTunnelingUiController->toggleSplitTunneling(true);
QVERIFY2(isSplitTunnelingChangedSpy.count() == 1, "isSplitTunnelingChangedSpy signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingUiController->isSplitTunnelingEnabled() == true, "AppSplitTunneling should be enabled");
m_coreController->m_appSplitTunnelingUiController->toggleSplitTunneling(false);
QVERIFY2(isSplitTunnelingChangedSpy.count() == 2, "isSplitTunnelingChangedSpy signal should be emitted 2nd time");
QVERIFY2(m_coreController->m_appSplitTunnelingUiController->isSplitTunnelingEnabled() == false, "AppSplitTunneling should be disabled");
const QString app = QCoreApplication::applicationFilePath();
m_coreController->m_appSplitTunnelingUiController->addApp(app);
m_coreController->m_appSplitTunnelingUiController->updateModel();
QVERIFY2(finishedSpy.count() == 1, "finished signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingModel->rowCount() == 1, "AppSplitTunnelingModel should have 1 row");
QModelIndex appSTModelIndex = m_coreController->m_appSplitTunnelingModel->index(0, 0);
QVERIFY2(appSTModelIndex.isValid(), "Site model index should be valid");
auto appPath = m_coreController->m_appSplitTunnelingModel->data(appSTModelIndex, AppSplitTunnelingModel::AppPathRole);
QString msg = QString("app path should be %1, got %2").arg(app, appPath.toString());
QVERIFY2(app.contains(appPath.toString()) == true, msg.toLocal8Bit().constData());
auto pkgAppName = m_coreController->m_appSplitTunnelingModel->data(appSTModelIndex, AppSplitTunnelingModel::PackageAppNameRole);
QVERIFY2(pkgAppName == true, "app name should be set");
auto pkgAppIcon = m_coreController->m_appSplitTunnelingModel->data(appSTModelIndex, AppSplitTunnelingModel::PackageAppIconRole);
QVERIFY2(pkgAppIcon == true, "app image should be set");
m_coreController->m_appSplitTunnelingUiController->addApp(app);
m_coreController->m_appSplitTunnelingUiController->updateModel();
QVERIFY2(errorOccurredSpy.count() == 1, "errorOccurred signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingModel->rowCount() == 1, "AppSplitTunnelingModel should have 3 rows (same app should not be added)");
m_coreController->m_appSplitTunnelingUiController->removeApp(0);
m_coreController->m_appSplitTunnelingUiController->updateModel();
QVERIFY2(finishedSpy.count() == 2, "finished signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingModel->rowCount() == 0, "AppSplitTunnelingModel should have 0 rows");
}
};
QTEST_MAIN(TestUiAppSTModelAndController)
#include "testUiAppSTModelAndController.moc"
+129
View File
@@ -0,0 +1,129 @@
#include <QDebug>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QUuid>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
class TestUiIpModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
QString normalizeHostname(const QString &hostname) const
{
QString normalized = hostname;
normalized.replace("https://", "");
normalized.replace("http://", "");
normalized.replace("ftp://", "");
normalized = normalized.split("/", Qt::SkipEmptyParts).first();
return normalized;
}
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 finishedSpy(m_coreController->m_ipSplitTunnelingUiController, &IpSplitTunnelingUiController::finished);
QSignalSpy errorOccurredSpy(m_coreController->m_ipSplitTunnelingUiController, &IpSplitTunnelingUiController::errorOccurred);
QSignalSpy isSplitTunnelingChangedSpy(m_coreController->m_ipSplitTunnelingUiController, &IpSplitTunnelingUiController::isSplitTunnelingEnabledChanged);
m_coreController->m_ipSplitTunnelingUiController->toggleSplitTunneling(true);
QVERIFY2(isSplitTunnelingChangedSpy.count() == 1, "isSplitTunnelingChangedSpy signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingUiController->isSplitTunnelingEnabled() == true, "ipSplitTunneling should be enabled");
m_coreController->m_ipSplitTunnelingUiController->toggleSplitTunneling(false);
QVERIFY2(isSplitTunnelingChangedSpy.count() == 2, "isSplitTunnelingChangedSpy signal should be emitted 2nd time");
QVERIFY2(m_coreController->m_ipSplitTunnelingUiController->isSplitTunnelingEnabled() == false, "ipSplitTunneling should be disabled");
QString site = "2ip.io";
m_coreController->m_ipSplitTunnelingUiController->addSite(site);
m_coreController->m_ipSplitTunnelingUiController->addSite("whatismyipaddress.com");
m_coreController->m_ipSplitTunnelingUiController->updateModel();
QVERIFY2(finishedSpy.count() == 2, "finished signal should be emitted 2 times");
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() == 2, "IpSplitTunnelingModel should have 2 rows");
QModelIndex siteModelIndex = m_coreController->m_ipSplitTunnelingModel->index(0, 0);
QVERIFY2(siteModelIndex.isValid(), "Ip model index should be valid");
auto siteUrl = m_coreController->m_ipSplitTunnelingModel->data(siteModelIndex, IpSplitTunnelingModel::UrlRole);
QCOMPARE(siteUrl, normalizeHostname(site));
auto siteIp = m_coreController->m_ipSplitTunnelingModel->data(siteModelIndex, IpSplitTunnelingModel::IpRole);
QVERIFY2(siteIp.isNull() == false, "Ip should not be empty");
m_coreController->m_ipSplitTunnelingUiController->removeSite(0);
m_coreController->m_ipSplitTunnelingUiController->updateModel();
QVERIFY2(finishedSpy.count() == 3, "finished signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() == 1, "IpSplitTunnelingModel should have 1 row");
QTemporaryDir tempDir;
QVERIFY2(tempDir.isValid(), "Temporary directory should be created");
const QString sitesFile = tempDir.filePath(QStringLiteral("sites.json"));
{
QFile file(sitesFile);
QVERIFY2(file.open(QIODevice::WriteOnly), "Sites import file should be writable");
file.write(R"([
{"hostname": "example.com", "ip": "93.184.216.34"},
{"hostname": "test.org", "ip": "1.2.3.4"}
])");
}
m_coreController->m_ipSplitTunnelingUiController->importSites(sitesFile, true);
m_coreController->m_ipSplitTunnelingUiController->updateModel();
QVERIFY2(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QVERIFY2(finishedSpy.count() == 4, "finished signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() > 1, "IpSplitTunnelingModel should have more than 1 row");
const QString exportFile = tempDir.filePath(QStringLiteral("test_ips_export.json"));
m_coreController->m_ipSplitTunnelingUiController->exportSites(exportFile);
QVERIFY2(finishedSpy.count() == 5, "finished signal should be emitted");
m_coreController->m_ipSplitTunnelingUiController->removeSites();
m_coreController->m_ipSplitTunnelingUiController->updateModel();
QVERIFY2(finishedSpy.count() == 6, "finished signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() == 0, "IpSplitTunnelingModel should have 0 rows");
}
};
QTEST_MAIN(TestUiIpModelAndController)
#include "testUiIpModelAndController.moc"
@@ -0,0 +1,91 @@
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QUuid>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
class TestUiLanguageModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
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 testChangeLanguage()
{
QVERIFY2(m_coreController->m_languageModel->rowCount() > 0, "Language model should not be empty");
QSignalSpy updateTranslationsSpy(m_coreController->m_languageUiController, &LanguageUiController::updateTranslations);
QSignalSpy translationsUpdatedSpy(m_coreController->m_languageUiController, &LanguageUiController::translationsUpdated);
m_coreController->m_languageUiController->changeLanguage(LanguageSettings::AvailableLanguageEnum::China_cn);
QVERIFY2(updateTranslationsSpy.count() == 1, "updateTranslations signal should be emitted");
QVERIFY2(translationsUpdatedSpy.count() == 1, "translationsUpdated signal should be emitted");
m_coreController->m_languageUiController->changeLanguage(LanguageSettings::AvailableLanguageEnum::English);
QVERIFY2(updateTranslationsSpy.count() == 2, "updateTranslations signal should be emitted");
QVERIFY2(translationsUpdatedSpy.count() == 2, "translationsUpdated signal should be emitted");
}
void testUrl()
{
m_coreController->m_languageUiController->changeLanguage(LanguageSettings::AvailableLanguageEnum::Russian);
QString siteRU = m_coreController->m_languageUiController->getCurrentSiteUrl("test_path");
QString docsRU = m_coreController->m_languageUiController->getCurrentDocsUrl("test_path");
m_coreController->m_languageUiController->changeLanguage(LanguageSettings::AvailableLanguageEnum::English);
QString siteEN = m_coreController->m_languageUiController->getCurrentSiteUrl("test_path");
QString docsEN = m_coreController->m_languageUiController->getCurrentDocsUrl("test_path");
QVERIFY2(siteRU != siteEN, "site url's should not be same");
QVERIFY2(docsRU != docsEN, "docs url's should not be same");
}
void testLineHeight()
{
m_coreController->m_languageUiController->changeLanguage(LanguageSettings::AvailableLanguageEnum::Burmese);
QVERIFY2(m_coreController->m_languageUiController->getLineHeightAppend() == 10, "line height should be 10");
m_coreController->m_languageUiController->changeLanguage(LanguageSettings::AvailableLanguageEnum::English);
QVERIFY2(m_coreController->m_languageUiController->getLineHeightAppend() == 0, "line height should be 0");
}
};
QTEST_MAIN(TestUiLanguageModelAndController)
#include "testUiLanguageModelAndController.moc"
@@ -0,0 +1,306 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDebug>
#include <QUuid>
#include <QSignalSpy>
#include <QModelIndex>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "core/controllers/selfhosted/importController.h"
#include "ui/models/serversModel.h"
#include "ui/models/containersModel.h"
#include "core/utils/constants/configKeys.h"
using namespace amnezia;
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/containerEnum.h"
#include "core/utils/protocolEnum.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
namespace {
int defaultServerRow(const QVector<ServerDescription> &descriptions, const QString &defaultServerId)
{
for (int i = 0; i < descriptions.size(); ++i) {
if (descriptions.at(i).serverId == defaultServerId) {
return i;
}
}
return -1;
}
} // namespace
class TestUiServersModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
QJsonObject createAwg2Config()
{
QJsonObject clientConfig;
clientConfig[configKey::mtu] = protocols::awg::defaultMtu;
clientConfig[configKey::junkPacketCount] = protocols::awg::defaultJunkPacketCount;
clientConfig[configKey::junkPacketMinSize] = protocols::awg::defaultJunkPacketMinSize;
clientConfig[configKey::junkPacketMaxSize] = protocols::awg::defaultJunkPacketMaxSize;
clientConfig[configKey::specialJunk1] = protocols::awg::defaultSpecialJunk1;
clientConfig[configKey::specialJunk2] = protocols::awg::defaultSpecialJunk2;
clientConfig[configKey::specialJunk3] = protocols::awg::defaultSpecialJunk3;
clientConfig[configKey::specialJunk4] = protocols::awg::defaultSpecialJunk4;
clientConfig[configKey::specialJunk5] = protocols::awg::defaultSpecialJunk5;
clientConfig[configKey::clientPrivKey] = "test_client_private_key";
clientConfig[configKey::clientPubKey] = "test_client_public_key";
clientConfig[configKey::serverPubKey] = "test_server_public_key";
clientConfig[configKey::pskKey] = "test_psk_key";
clientConfig[configKey::clientIp] = "10.8.1.2";
clientConfig[configKey::allowedIps] = QJsonArray::fromStringList({"0.0.0.0/0"});
QJsonObject awgConfig;
awgConfig[configKey::lastConfig] = QString(QJsonDocument(clientConfig).toJson());
awgConfig[configKey::port] = protocols::awg::defaultPort;
awgConfig[configKey::transportProto] = "udp";
awgConfig[configKey::protocolVersion] = protocols::awg::awgV2;
awgConfig[configKey::subnetAddress] = protocols::wireguard::defaultSubnetAddress;
awgConfig[configKey::junkPacketCount] = protocols::awg::defaultJunkPacketCount;
awgConfig[configKey::junkPacketMinSize] = protocols::awg::defaultJunkPacketMinSize;
awgConfig[configKey::junkPacketMaxSize] = protocols::awg::defaultJunkPacketMaxSize;
awgConfig[configKey::initPacketJunkSize] = protocols::awg::initPacketJunkSizeMax;
awgConfig[configKey::responsePacketJunkSize] = protocols::awg::responsePacketJunkSizeMax;
awgConfig[configKey::cookieReplyPacketJunkSize] = protocols::awg::cookieReplyPacketJunkSizeMax;
awgConfig[configKey::transportPacketJunkSize] = protocols::awg::defaultPadding;
awgConfig[configKey::initPacketMagicHeader] = protocols::awg::defaultInitPacketMagicHeader;
awgConfig[configKey::responsePacketMagicHeader] = protocols::awg::defaultResponsePacketMagicHeader;
awgConfig[configKey::underloadPacketMagicHeader] = protocols::awg::defaultUnderloadPacketMagicHeader;
awgConfig[configKey::transportPacketMagicHeader] = protocols::awg::defaultTransportPacketMagicHeader;
awgConfig[configKey::specialJunk1] = protocols::awg::defaultSpecialJunk1;
awgConfig[configKey::specialJunk2] = protocols::awg::defaultSpecialJunk2;
awgConfig[configKey::specialJunk3] = protocols::awg::defaultSpecialJunk3;
awgConfig[configKey::specialJunk4] = protocols::awg::defaultSpecialJunk4;
awgConfig[configKey::specialJunk5] = protocols::awg::defaultSpecialJunk5;
awgConfig[configKey::isThirdPartyConfig] = true;
QJsonObject container;
container[configKey::container] = "amnezia-awg";
container[configKey::awg] = awgConfig;
QJsonArray containers;
containers.append(container);
QJsonObject config;
config[configKey::containers] = containers;
config[configKey::defaultContainer] = "amnezia-awg";
config[configKey::description] = "AWG2 Test Server";
config[configKey::hostName] = "test.example.com";
return config;
}
QJsonObject createServerDescriptionTestConfig(bool withAmneziaDns)
{
QJsonObject config = createAwg2Config();
config[configKey::description] = "Server 1";
if (withAmneziaDns) {
config[configKey::dns1] = protocols::dns::amneziaDnsIp;
}
return config;
}
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 testUiServersModelAndControllerRoles() {
QJsonObject testConfig = createAwg2Config();
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
m_coreController->m_importCoreController->importConfig(testConfig);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server should be imported");
int serverIndex = m_coreController->m_serversRepository->defaultServerIndex();
QVERIFY2(serverIndex == 0, "Default server index should be 0");
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "ServersModel should have 1 row");
QModelIndex serverModelIndex = m_coreController->m_serversModel->index(0, 0);
QVERIFY2(serverModelIndex.isValid(), "Server model index should be valid");
QString serverName = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::NameRole).toString();
QVERIFY2(serverName == "AWG2 Test Server", QString("Server name should be 'AWG2 Test Server', got '%1'").arg(serverName).toUtf8().constData());
QString serverDescription = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::ServerDescriptionRole).toString();
QVERIFY2(serverDescription.contains("test.example.com"), QString("Server description should contain hostname, got '%1'").arg(serverDescription).toUtf8().constData());
QString hostName = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HostNameRole).toString();
QVERIFY2(hostName == "test.example.com", "Host name should match");
bool isDefault = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::IsDefaultRole).toBool();
QVERIFY2(isDefault == true, "Server should be default");
bool hasInstalledContainers = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HasInstalledContainers).toBool();
QVERIFY2(hasInstalledContainers == true, "Server should have installed containers");
bool hasWriteAccess = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HasWriteAccessRole).toBool();
QVERIFY2(hasWriteAccess == false, "Server should not have write access for imported config");
int defaultContainerRole = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::DefaultContainerRole).toInt();
DockerContainer expectedContainer = DockerContainer::Awg;
QVERIFY2(defaultContainerRole == static_cast<int>(expectedContainer), "Default container should be Awg");
}
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(
m_coreController->m_serversUiController->getServerId(0));
QString hostName = "test.example.com";
QString collapsedDescription = m_coreController->m_serversUiController->getDefaultServerDescriptionCollapsed();
QString expectedCollapsed = "AmneziaWG (version 2) | " + hostName;
QVERIFY2(collapsedDescription == expectedCollapsed,
QString("Collapsed description should be '%1', got '%2'").arg(expectedCollapsed, collapsedDescription).toUtf8().constData());
QString expandedDescription = m_coreController->m_serversUiController->getDefaultServerDescriptionExpanded();
QString expectedExpanded = hostName;
QVERIFY2(expandedDescription == expectedExpanded,
QString("Expanded description should be '%1', got '%2'").arg(expectedExpanded, expandedDescription).toUtf8().constData());
}
if (m_coreController->m_containersModel) {
int awgContainerIndex = -1;
for (int i = 0; i < ContainerUtils::allContainers().size(); ++i) {
DockerContainer container = ContainerUtils::allContainers().at(i);
if (container == DockerContainer::Awg) {
awgContainerIndex = i;
break;
}
}
QVERIFY2(awgContainerIndex >= 0, "Awg container index should be found");
QModelIndex containerModelIndex = m_coreController->m_containersModel->index(awgContainerIndex, 0);
QVERIFY2(containerModelIndex.isValid(), "Container model index should be valid");
bool isInstalled = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsInstalledRole).toBool();
QVERIFY2(isInstalled == true, "Awg container should be installed");
bool isVpnContainer = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsVpnContainerRole).toBool();
QVERIFY2(isVpnContainer == true, "Awg container should be VPN container");
QString containerName = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::NameRole).toString();
QString expectedContainerName = ContainerUtils::containerHumanNames().value(DockerContainer::Awg);
QVERIFY2(containerName == expectedContainerName, QString("Container name should be '%1', got '%2'").arg(expectedContainerName, containerName).toUtf8().constData());
QString containerDescription = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DescriptionRole).toString();
QString expectedDescription = ContainerUtils::containerDescriptions().value(DockerContainer::Awg);
QVERIFY2(containerDescription == expectedDescription, QString("Container description should match, got '%1'").arg(containerDescription).toUtf8().constData());
QString detailedDescription = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DetailedDescriptionRole).toString();
QString expectedDetailedDescription = ContainerUtils::containerDetailedDescriptions().value(DockerContainer::Awg);
QVERIFY2(detailedDescription == expectedDetailedDescription, QString("Container detailed description should match, got '%1'").arg(detailedDescription).toUtf8().constData());
int serviceType = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ServiceTypeRole).toInt();
QVERIFY2(serviceType == static_cast<int>(ProtocolEnumNS::ServiceType::Vpn), "Service type should be Vpn");
bool isSupported = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsSupportedRole).toBool();
QVERIFY2(isSupported == true, "Container should be supported");
bool isShareable = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsShareableRole).toBool();
QVERIFY2(isShareable == true, "Container should be shareable");
QJsonObject containerConfig = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ConfigRole).toJsonObject();
QVERIFY2(!containerConfig.isEmpty(), "Container config should not be empty");
QVERIFY2(containerConfig.value(configKey::container).toString() == "amnezia-awg", "Container config should have correct container type");
QJsonObject awgProtocolConfig = containerConfig.value(configKey::awg).toObject();
QVERIFY2(!awgProtocolConfig.isEmpty(), "AWG protocol config should not be empty");
QString protocolVersion = awgProtocolConfig.value(configKey::protocolVersion).toString();
QVERIFY2(protocolVersion == protocols::awg::awgV2, QString("Protocol version should be '%1', got '%2'").arg(protocols::awg::awgV2, protocolVersion).toUtf8().constData());
QString port = awgProtocolConfig.value(configKey::port).toString();
QVERIFY2(port == protocols::awg::defaultPort, QString("Port should be '%1', got '%2'").arg(protocols::awg::defaultPort, port).toUtf8().constData());
QString subnetAddress = awgProtocolConfig.value(configKey::subnetAddress).toString();
QVERIFY2(subnetAddress == protocols::wireguard::defaultSubnetAddress, QString("Subnet address should be '%1', got '%2'").arg(protocols::wireguard::defaultSubnetAddress, subnetAddress).toUtf8().constData());
bool isThirdParty = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsThirdPartyConfigRole).toBool();
QVERIFY2(isThirdParty == true, "Imported config should be third party config");
DockerContainer dockerContainer = static_cast<DockerContainer>(m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DockerContainerRole).toInt());
QVERIFY2(dockerContainer == DockerContainer::Awg, "Docker container should be Awg");
QString containerString = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ContainerStringRole).toString();
QVERIFY2(containerString == "amnezia-awg", "Container string should be amnezia-awg");
}
}
void testServerDescriptionFormat() {
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QJsonObject configNoDns = createServerDescriptionTestConfig(false);
m_coreController->m_importCoreController->importConfig(configNoDns);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished should be emitted");
m_coreController->m_appSettingsRepository->setUseAmneziaDns(false);
QVector<ServerDescription> descriptionsNoDns = m_coreController->m_serversController->buildServerDescriptions(
m_coreController->m_appSettingsRepository->useAmneziaDns());
const QString defIdNoDns = m_coreController->m_serversRepository->defaultServerId();
m_coreController->m_serversModel->updateModel(descriptionsNoDns, defIdNoDns);
QString descNoDns = m_coreController->m_serversModel->data(
m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString();
QVERIFY2(descNoDns == "test.example.com",
QString("Without Amnezia DNS expected 'test.example.com', got '%1'").arg(descNoDns).toUtf8().constData());
m_coreController->m_serversRepository->clearServers();
if (m_coreController->m_serversRepository->serversCount() > 0) {
m_coreController->m_serversRepository->setDefaultServer(m_coreController->m_serversRepository->serverIdAt(0));
}
QJsonObject configWithDns = createServerDescriptionTestConfig(true);
m_coreController->m_importCoreController->importConfig(configWithDns);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server should be imported");
m_coreController->m_appSettingsRepository->setUseAmneziaDns(true);
QVector<ServerDescription> descriptionsWithDns = m_coreController->m_serversController->buildServerDescriptions(
m_coreController->m_appSettingsRepository->useAmneziaDns());
const QString defIdWithDns = m_coreController->m_serversRepository->defaultServerId();
m_coreController->m_serversModel->updateModel(descriptionsWithDns, defIdWithDns);
QString descWithDns = m_coreController->m_serversModel->data(
m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString();
QVERIFY2(descWithDns == "Amnezia DNS | test.example.com",
QString("With Amnezia DNS expected 'Amnezia DNS | test.example.com', got '%1'").arg(descWithDns).toUtf8().constData());
}
};
QTEST_MAIN(TestUiServersModelAndController)
#include "testUiServersModelAndController.moc"
+290
View File
@@ -0,0 +1,290 @@
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QUuid>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "core/utils/serialization/serialization.h"
#include "core/utils/utilities.h"
#include "secureQSettings.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
using namespace amnezia;
using namespace amnezia::test;
class TestXraySerialization : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
QJsonObject extractXrayConfig(const QString &data, ConfigTypes configType, const QString &description = "") const
{
QJsonParseError parserErr;
QJsonDocument jsonConf = QJsonDocument::fromJson(data.toLocal8Bit(), &parserErr);
QJsonObject xrayVpnConfig;
xrayVpnConfig[configKey::config] = jsonConf.toJson().constData();
QJsonObject lastConfig;
lastConfig[configKey::lastConfig] = jsonConf.toJson().constData();
lastConfig[configKey::isThirdPartyConfig] = true;
QJsonObject containers;
if (configType == ConfigTypes::ShadowSocks) {
containers.insert(configKey::ssxray, QJsonValue(lastConfig));
containers.insert(configKey::container, QJsonValue(configKey::amneziaSsxray));
} else {
containers.insert(configKey::container, QJsonValue(configKey::amneziaXray));
containers.insert(configKey::xray, QJsonValue(lastConfig));
}
QJsonArray arr;
arr.push_back(containers);
QString hostName;
const static QRegularExpression hostNameRegExp("\"address\":\\s*\"([^\"]+)");
QRegularExpressionMatch hostNameMatch = hostNameRegExp.match(data);
if (hostNameMatch.hasMatch()) {
hostName = hostNameMatch.captured(1);
}
QJsonObject config;
config[configKey::containers] = arr;
config[configKey::defaultContainer] =
(configType == ConfigTypes::ShadowSocks) ? configKey::amneziaSsxray : configKey::amneziaXray;
if (description.isEmpty()) {
config[configKey::description] = m_coreController->m_serversRepository->nextAvailableServerName();
} else {
config[configKey::description] = description;
}
config[configKey::hostName] = hostName;
return config;
}
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 testVless()
{
const QString configData = getEnvValue("THIRD_PARTY_VLESS_IMPORT_DATA");
if (!isEnvValueConfigured(configData)) {
QSKIP("THIRD_PARTY_VLESS_IMPORT_DATA is not configured");
}
ImportController::ImportResult importResult;
m_coreController->m_importCoreController->extractConfigFromData(configData);
QString config = configData;
QString prefix;
QString errormsg;
ConfigTypes configType = ConfigTypes::Invalid;
if (config.startsWith("vless://")) {
configType = ConfigTypes::Xray;
importResult.config = extractXrayConfig(
Utils::JsonToString(serialization::vless::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact),
configType, prefix);
QVERIFY2(!importResult.config.empty(), "Config shouldn't be empty");
} else {
QSKIP("Config not starts with vless://");
}
QCOMPARE(importResult.config, config);
}
void testVmessNew()
{
const QString configData = getEnvValue("THIRD_PARTY_VMESS_NEW_IMPORT_DATA");
if (!isEnvValueConfigured(configData)) {
QSKIP("THIRD_PARTY_VMESS_NEW_IMPORT_DATA is not configured");
}
QString clientName = "Test Client (vmess_new deserialization)";
ImportController::ImportResult importResult;
m_coreController->m_importCoreController->extractConfigFromData(configData);
QString config = configData;
QString prefix;
QString errormsg;
ConfigTypes configType = ConfigTypes::Invalid;
if (config.startsWith("vmess://") && config.contains("@")) {
configType = ConfigTypes::Xray;
importResult.config = extractXrayConfig(
Utils::JsonToString(serialization::vmess_new::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact),
configType, prefix);
QVERIFY2(!importResult.config.empty(), "Config shouldn't be empty");
} else {
QSKIP("Config not starts with vmess:// or not contain @");
}
QCOMPARE(importResult.config, config);
}
void testVmess()
{
const QString configData = getEnvValue("THIRD_PARTY_VMESS_IMPORT_DATA");
if (!isEnvValueConfigured(configData)) {
QSKIP("THIRD_PARTY_VMESS_IMPORT_DATA is not configured");
}
QString clientName = "Test Client (vmess deserialization)";
ImportController::ImportResult importResult;
m_coreController->m_importCoreController->extractConfigFromData(configData);
QString config = configData;
QString prefix;
QString errormsg;
ConfigTypes configType = ConfigTypes::Invalid;
if (config.startsWith("vmess://")) {
configType = ConfigTypes::Xray;
importResult.config = extractXrayConfig(
Utils::JsonToString(serialization::vmess::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact),
configType, prefix);
QVERIFY2(!importResult.config.empty(), "Config shouldn't be empty");
} else {
QSKIP("Config not starts with vmess://");
}
QCOMPARE(importResult.config, config);
}
void testTrojan()
{
const QString configData = getEnvValue("THIRD_PARTY_TROJAN_IMPORT_DATA");
if (!isEnvValueConfigured(configData)) {
QSKIP("THIRD_PARTY_TROJAN_IMPORT_DATA is not configured");
}
QString clientName = "Test Client (trojan deserialization)";
ImportController::ImportResult importResult;
m_coreController->m_importCoreController->extractConfigFromData(configData);
QString config = configData;
QString prefix;
QString errormsg;
ConfigTypes configType = ConfigTypes::Invalid;
if (config.startsWith("trojan://")) {
configType = ConfigTypes::Xray;
importResult.config = extractXrayConfig(
Utils::JsonToString(serialization::trojan::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact),
configType, prefix);
QVERIFY2(!importResult.config.empty(), "Config shouldn't be empty");
} else {
QSKIP("Config not starts with trojan://");
}
QCOMPARE(importResult.config, config);
}
void testSS()
{
const QString configData = getEnvValue("THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA");
if (!isEnvValueConfigured(configData)) {
QSKIP("THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA is not configured");
}
QString clientName = "Test Client (ss deserialization)";
ImportController::ImportResult importResult;
m_coreController->m_importCoreController->extractConfigFromData(configData);
QString config = configData;
QString prefix;
QString errormsg;
ConfigTypes configType = ConfigTypes::Invalid;
if (config.startsWith("ss://") && !config.contains("plugin=")) {
configType = ConfigTypes::ShadowSocks;
importResult.config = extractXrayConfig(
Utils::JsonToString(serialization::ss::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact),
configType, prefix);
QVERIFY2(!importResult.config.empty(), "Config shouldn't be empty");
} else {
QSKIP("Config not starts with ss:// or contain plugin=");
}
QCOMPARE(importResult.config, config);
}
void testSSd()
{
const QString configData = getEnvValue("THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA");
if (!isEnvValueConfigured(configData)) {
QSKIP("THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA is not configured");
}
QString clientName = "Test Client (ssd deserialization)";
ImportController::ImportResult importResult;
m_coreController->m_importCoreController->extractConfigFromData(configData);
QString config = configData;
QString prefix;
QString errormsg;
ConfigTypes configType = ConfigTypes::Invalid;
if (config.startsWith("ssd://")) {
QStringList tmp;
QList<std::pair<QString, QJsonObject>> servers = serialization::ssd::Deserialize(config, &prefix, &tmp);
configType = ConfigTypes::ShadowSocks;
// Took only first config from list
if (!servers.isEmpty()) {
importResult.config = extractXrayConfig(servers.first().first, configType);
}
if (!importResult.config.empty()) {
importResult.configType = configType;
}
QVERIFY2(!importResult.config.empty(), "Config shouldn't be empty");
} else {
QSKIP("Config not starts with ssd://");
}
QCOMPARE(importResult.config, config);
}
};
QTEST_MAIN(TestXraySerialization)
#include "testXraySerialization.moc"
+76
View File
@@ -0,0 +1,76 @@
#ifndef TESTCORECONTROLLER_H
#define TESTCORECONTROLLER_H
#include "core/controllers/coreController.h"
class TestCoreController final : public CoreController
{
public:
explicit TestCoreController(const QSharedPointer<VpnConnection> &vpnConnection,
SecureQSettings *settings,
QQmlApplicationEngine *engine,
QObject *parent = nullptr)
: CoreController(vpnConnection, settings, engine, parent, /*skipPlatformControllerInit=*/true)
, m_serversRepository(serversRepositoryProtected())
, m_appSettingsRepository(appSettingsRepositoryProtected())
, m_serversModel(serversModelProtected())
, m_containersModel(containersModelProtected())
, m_apiServicesModel(apiServicesModelProtected())
, m_newsModel(newsModelProtected())
, m_allowedDnsModel(allowedDnsModelProtected())
, m_appSplitTunnelingModel(appSplitTunnelingModelProtected())
, m_ipSplitTunnelingModel(ipSplitTunnelingModelProtected())
, m_languageModel(languageModelProtected())
, m_connectionUiController(connectionUiControllerProtected())
, m_installUiController(installUiControllerProtected())
, m_importCoreController(importCoreControllerProtected())
, m_exportController(exportControllerProtected())
, m_installController(installControllerProtected())
, m_serversController(serversControllerProtected())
, m_settingsUiController(settingsUiControllerProtected())
, m_settingsController(settingsControllerProtected())
, m_allowedDnsUiController(allowedDnsUiControllerProtected())
, m_allowedDnsController(allowedDnsControllerProtected())
, m_languageUiController(languageUiControllerProtected())
, m_ipSplitTunnelingController(ipSplitTunnelingControllerProtected())
, m_ipSplitTunnelingUiController(ipSplitTunnelingUiControllerProtected())
, m_appSplitTunnelingController(appSplitTunnelingControllerProtected())
, m_appSplitTunnelingUiController(appSplitTunnelingUiControllerProtected())
, m_serversUiController(serversUiControllerProtected())
, m_servicesCatalogUiController(servicesCatalogUiControllerProtected())
, m_apiNewsUiController(apiNewsUiControllerProtected())
{
}
SecureServersRepository *m_serversRepository;
SecureAppSettingsRepository *m_appSettingsRepository;
ServersModel *m_serversModel;
ContainersModel *m_containersModel;
ApiServicesModel *m_apiServicesModel;
NewsModel *m_newsModel;
AllowedDnsModel *m_allowedDnsModel;
AppSplitTunnelingModel *m_appSplitTunnelingModel;
IpSplitTunnelingModel *m_ipSplitTunnelingModel;
LanguageModel *m_languageModel;
ConnectionUiController *m_connectionUiController;
InstallUiController *m_installUiController;
ImportController *m_importCoreController;
ExportController *m_exportController;
InstallController *m_installController;
ServersController *m_serversController;
SettingsUiController *m_settingsUiController;
SettingsController *m_settingsController;
AllowedDnsUiController *m_allowedDnsUiController;
AllowedDnsController *m_allowedDnsController;
LanguageUiController *m_languageUiController;
IpSplitTunnelingController *m_ipSplitTunnelingController;
IpSplitTunnelingUiController *m_ipSplitTunnelingUiController;
AppSplitTunnelingController *m_appSplitTunnelingController;
AppSplitTunnelingUiController *m_appSplitTunnelingUiController;
ServersUiController *m_serversUiController;
ServicesCatalogUiController *m_servicesCatalogUiController;
ApiNewsUiController *m_apiNewsUiController;
};
#endif // TESTCORECONTROLLER_H
+73
View File
@@ -0,0 +1,73 @@
#include "utils/testUtils.h"
#include <QByteArray>
#include <QDebug>
#include "core/repositories/secureServersRepository.h"
#include "core/utils/serverConfigUtils.h"
namespace amnezia::test
{
QString getEnvValue(const QString &key)
{
const QByteArray keyUtf8 = key.toUtf8();
if (qEnvironmentVariableIsSet(keyUtf8.constData())) {
return qEnvironmentVariable(keyUtf8.constData());
}
return QString();
}
void logEnvValueState(const QString &key)
{
const QString value = getEnvValue(key);
qInfo().noquote() << QString("%1: configured=%2 length=%3 value=\"%4\"")
.arg(key,
isEnvValueConfigured(value) ? "yes" : "no",
QString::number(value.size()),
value);
}
bool isEnvValueConfigured(const QString &value)
{
const QString trimmed = value.trimmed();
if (trimmed.isEmpty()) {
return false;
}
if (trimmed.endsWith(QStringLiteral("_config_string"))) {
return false;
}
return true;
}
std::optional<QString> serverDescription(SecureServersRepository *repo, const QString &serverId)
{
switch (repo->serverKind(serverId)) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = repo->selfHostedAdminConfig(serverId);
return cfg ? std::optional<QString>(cfg->description) : std::nullopt;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = repo->selfHostedUserConfig(serverId);
return cfg ? std::optional<QString>(cfg->description) : std::nullopt;
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = repo->nativeConfig(serverId);
return cfg ? std::optional<QString>(cfg->description) : std::nullopt;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2: {
const auto cfg = repo->apiV2Config(serverId);
return cfg ? std::optional<QString>(cfg->description) : std::nullopt;
}
default:
return std::nullopt;
}
}
std::optional<QString> serverDescriptionAt(SecureServersRepository *repo, int index)
{
return serverDescription(repo, repo->serverIdAt(index));
}
} // namespace amnezia::test
+26
View File
@@ -0,0 +1,26 @@
#ifndef TESTUTILS_H
#define TESTUTILS_H
#include <optional>
#include <QString>
class SecureServersRepository;
namespace amnezia::test
{
QString getEnvValue(const QString &key);
bool isEnvValueConfigured(const QString &value);
// Logs the env var state and value. Secret values coming from GitHub Actions
// secrets are masked as *** in CI logs automatically.
void logEnvValueState(const QString &key);
std::optional<QString> serverDescription(SecureServersRepository *repo, const QString &serverId);
std::optional<QString> serverDescriptionAt(SecureServersRepository *repo, int index);
} // namespace amnezia::test
#endif // TESTUTILS_H
+296
View File
@@ -0,0 +1,296 @@
#!/usr/bin/env bash
# Run Android tests locally on a connected device or emulator.
#
# ABI guidance:
# x86_64 (default) – use for emulators; HVF/Rosetta hardware acceleration works on
# both Intel and Apple Silicon Macs, so OpenGL doesn't crash.
# arm64-v8a – use for physical ARM devices only; arm64-v8a emulators on
# Apple Silicon have OpenGL emulation bugs that crash Qt apps.
#
# Prerequisites:
# - Qt for Android x86_64 (or arm64-v8a for devices) installed via Qt Maintenance Tool
# - Android SDK + NDK available via Android Studio
# - `adb devices` shows at least one device/emulator
# - Conan 2 installed (pip install "conan==2.28.0")
#
# Usage:
# bash run_android_tests.sh # x86_64 emulator (default)
# bash run_android_tests.sh --abi arm64-v8a # physical ARM device
# bash run_android_tests.sh --test openssl # only openssl test
# bash run_android_tests.sh --test restore_backup # only restore-backup test
# bash run_android_tests.sh --wait-boot # wait for emulator to boot first
set -euo pipefail
# ── Configurable defaults ──────────────────────────────────────────────────────
QT_ROOT="${QT_ROOT:-/Users/nickpc/Qt/6.10.1}"
ANDROID_SDK="${ANDROID_HOME:-/Users/nickpc/Library/Android/sdk}"
NDK_VERSION="${NDK_VERSION:-27.3.13750724}"
ANDROID_PLATFORM="${ANDROID_PLATFORM:-android-28}"
# x86_64: recommended for emulators (HVF/Rosetta hardware acceleration works, no GL crashes)
# arm64-v8a: for physical ARM devices only (arm64 emulators have OpenGL bugs on Apple Silicon)
ABI="${ABI:-x86_64}"
ABI_DIR="${ABI//-/_}"
BUILD_DIR="${BUILD_DIR:-build/android_${ABI_DIR}}"
TEST_FILTER="all" # "all" | "openssl" | "restore_backup"
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
# ──────────────────────────────────────────────────────────────────────────────
# Parse CLI flags
while [[ $# -gt 0 ]]; do
case $1 in
--abi) ABI="$2"; ABI_DIR="${ABI//-/_}"; BUILD_DIR="build/android_${ABI_DIR}"; shift 2 ;;
--test) TEST_FILTER="$2"; shift 2 ;;
--wait-boot) WAIT_BOOT=true; shift ;;
--device) DEVICE_SERIAL="$2"; shift 2 ;;
--list-devices) adb devices -l; exit 0 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
# Apply device selection
if [[ -n "$DEVICE_SERIAL" ]]; then
export ANDROID_SERIAL="$DEVICE_SERIAL"
echo ">>> Using device: $ANDROID_SERIAL"
elif [[ -n "${ANDROID_SERIAL:-}" ]]; then
echo ">>> Using device from env: $ANDROID_SERIAL"
fi
# Map ABI to Qt arch directory name
case "$ABI" in
arm64-v8a) QT_ARCH_DIR="android_arm64_v8a" ;;
x86_64) QT_ARCH_DIR="android_x86_64" ;;
armeabi-v7a) QT_ARCH_DIR="android_armv7" ;;
x86) QT_ARCH_DIR="android_x86" ;;
*) echo "Unsupported ABI: $ABI"; exit 1 ;;
esac
QT_ANDROID="$QT_ROOT/$QT_ARCH_DIR"
QT_HOST="$QT_ROOT/macos"
NDK_PATH="$ANDROID_SDK/ndk/$NDK_VERSION"
echo "=== Android test runner (local) ==="
echo " ABI : $ABI"
echo " Qt Android : $QT_ANDROID"
echo " Qt Host : $QT_HOST"
echo " NDK : $NDK_PATH"
echo " Build dir : $BUILD_DIR"
echo " Platform : $ANDROID_PLATFORM"
echo ""
# Sanity checks
[[ -d "$QT_ANDROID" ]] || { echo "ERROR: Qt Android not found at $QT_ANDROID"; exit 1; }
[[ -d "$NDK_PATH" ]] || { echo "ERROR: NDK not found at $NDK_PATH"; exit 1; }
command -v adb >/dev/null || { echo "ERROR: adb not found in PATH"; exit 1; }
if $WAIT_BOOT; then
echo ">>> Waiting for emulator to finish booting..."
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done'
echo " Emulator ready."
fi
# List connected devices for visibility
echo ">>> Connected devices:"
adb devices -l | tail -n +2 | grep -v "^$" || true
echo ""
adb devices | grep -qE "device$|emulator" || {
echo "ERROR: No Android device/emulator connected."
echo " Run: bash run_android_tests.sh --list-devices"
echo " Then: bash run_android_tests.sh --device <serial>"
exit 1
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# ── Locate cmake ──────────────────────────────────────────────────────────────
if ! command -v cmake &>/dev/null; then
QT_CMAKE="/Users/nickpc/Qt/Tools/CMake/CMake.app/Contents/bin"
if [[ -x "$QT_CMAKE/cmake" ]]; then
export PATH="$QT_CMAKE:$PATH"
echo ">>> Using Qt-bundled cmake: $QT_CMAKE/cmake"
else
echo "ERROR: cmake not found. Install it or add it to PATH."
exit 1
fi
fi
# ── Locate ninja ──────────────────────────────────────────────────────────────
if ! command -v ninja &>/dev/null; then
QT_NINJA="/Users/nickpc/Qt/Tools/Ninja"
if [[ -x "$QT_NINJA/ninja" ]]; then
export PATH="$QT_NINJA:$PATH"
echo ">>> Using Qt-bundled ninja: $QT_NINJA/ninja"
else
echo "ERROR: ninja not found. Install it (brew install ninja) or add it to PATH."
exit 1
fi
fi
# ── Add NDK toolchain to PATH (provides clang / clang++) ─────────────────────
NDK_TOOLCHAIN="$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin"
if [[ -d "$NDK_TOOLCHAIN" ]]; then
export PATH="$NDK_TOOLCHAIN:$PATH"
fi
# ── Required env vars for androiddeployqt ────────────────────────────────────
export ANDROID_NDK_ROOT="$NDK_PATH"
export ANDROID_SDK_ROOT="$ANDROID_SDK"
# Auto-detect JAVA_HOME (Android Studio or system JDK)
if [[ -z "${JAVA_HOME:-}" ]]; then
for candidate in \
"/Applications/Android Studio.app/Contents/jbr/Contents/Home" \
"/Applications/Android Studio.app/Contents/jre/Contents/Home" \
"$(/usr/libexec/java_home 2>/dev/null || true)"
do
if [[ -x "$candidate/bin/java" ]]; then
export JAVA_HOME="$candidate"
break
fi
done
fi
[[ -n "${JAVA_HOME:-}" ]] || { echo "ERROR: JAVA_HOME not set and no JDK found. Install Android Studio or JDK 17."; exit 1; }
echo ">>> JAVA_HOME: $JAVA_HOME"
# ── Configure ─────────────────────────────────────────────────────────────────
# Qt's own toolchain file sets CMAKE_PREFIX_PATH, ABI, NDK, and sysroot
# correctly. Using raw CMake Android variables causes Qt6 to not be found
# because the NDK toolchain resets find paths.
QT_TOOLCHAIN="$QT_ANDROID/lib/cmake/Qt6/qt.toolchain.cmake"
[[ -f "$QT_TOOLCHAIN" ]] || { echo "ERROR: Qt toolchain not found: $QT_TOOLCHAIN"; exit 1; }
# Detect the actual NDK host tag from what's present in the NDK
# (Apple Silicon Macs have only darwin-x86_64 in NDK ≤27, not darwin-aarch64)
NDK_HOST_TAG=""
for candidate in darwin-x86_64 darwin-aarch64 linux-x86_64; do
if [[ -d "$NDK_PATH/toolchains/llvm/prebuilt/$candidate" ]]; then
NDK_HOST_TAG="$candidate"
break
fi
done
[[ -n "$NDK_HOST_TAG" ]] || { echo "ERROR: Cannot detect NDK host tag in $NDK_PATH/toolchains/llvm/prebuilt/"; exit 1; }
echo ">>> NDK host tag: $NDK_HOST_TAG"
# ABI → sysroot triple mapping for libc++_shared.so
case "$ABI" in
arm64-v8a) SYSROOT_TRIPLE="aarch64-linux-android" ;;
x86_64) SYSROOT_TRIPLE="x86_64-linux-android" ;;
armeabi-v7a) SYSROOT_TRIPLE="arm-linux-androideabi" ;;
x86) SYSROOT_TRIPLE="i686-linux-android" ;;
esac
# androiddeployqt appends "/<triple>/libc++_shared.so" itself, so point to usr/lib/, not usr/lib/<triple>/
STD_CXX_PATH="$NDK_PATH/toolchains/llvm/prebuilt/$NDK_HOST_TAG/sysroot/usr/lib"
echo ">>> Configuring..."
cmake -S . -B "$BUILD_DIR" -G Ninja \
-DCMAKE_TOOLCHAIN_FILE="$QT_TOOLCHAIN" \
-DCMAKE_BUILD_TYPE=Debug \
-DANDROID_PLATFORM="$ANDROID_PLATFORM" \
-DANDROID_SDK_ROOT="$ANDROID_SDK" \
-DCMAKE_ANDROID_NDK="$NDK_PATH" \
-DCMAKE_ANDROID_NDK_TOOLCHAIN_HOST_TAG="$NDK_HOST_TAG" \
-DQT_HOST_PATH="$QT_HOST" \
-DQt6_DIR="$QT_ANDROID/lib/cmake/Qt6" \
-DCMAKE_PREFIX_PATH="$QT_ANDROID"
# Patch deployment settings JSONs: cmake may leave ndk-host empty on Apple Silicon
for json_file in "$BUILD_DIR"/tests/android-*-deployment-settings.json; do
[[ -f "$json_file" ]] || continue
python3 - "$json_file" "$NDK_HOST_TAG" "$STD_CXX_PATH" <<'PYEOF'
import sys, json
path, host_tag, stl_path = sys.argv[1], sys.argv[2], sys.argv[3]
with open(path) as f:
d = json.load(f)
changed = False
if not d.get("ndk-host"):
d["ndk-host"] = host_tag
changed = True
if d.get("stdcpp-path", "").startswith("/usr/lib/") or "/usr/lib//" in d.get("stdcpp-path", ""):
d["stdcpp-path"] = stl_path + "/"
changed = True
if changed:
with open(path, "w") as f:
json.dump(d, f, indent=3)
print(f" Patched {path}")
PYEOF
done
ADB_BIN=$(which adb)
run_test() {
local target="$1"
local apk_dir="$BUILD_DIR/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"
echo ""
echo ">>> Building $target APK..."
cmake --build "$BUILD_DIR" --target "${target}_make_apk"
echo ">>> Installing $target APK on device..."
"$ADB_BIN" install -r "$apk"
# Pre-create files/stdout.txt before launching so the test process finds it
# immediately and androidtestrunner-style tail won't fail on a missing file.
"$ADB_BIN" shell "run-as $pkg_name sh -c 'mkdir -p files && : > files/stdout.txt'" 2>/dev/null || true
# Remove stale exit code from any previous run
"$ADB_BIN" shell "run-as $pkg_name sh -c 'rm -f files/qtest_last_exit_code'" 2>/dev/null || true
echo ">>> Launching $target..."
"$ADB_BIN" shell am start -n "$activity"
# Poll for qtest_last_exit_code (written by Qt's test runner on exit)
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_BIN" shell \
"run-as $pkg_name cat files/qtest_last_exit_code 2>/dev/null" \
| tr -d '[:space:]' || true)
[[ -n "$exit_code" ]] && break
done
echo ""
local stdout_content
stdout_content=$("$ADB_BIN" shell \
"run-as $pkg_name cat files/stdout.txt 2>/dev/null" || true)
if [[ -n "$stdout_content" ]]; then
echo ">>> Test output (files/stdout.txt):"
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 tests ─────────────────────────────────────────────────────────────────
case "$TEST_FILTER" in
all)
run_test test_android_openssl
run_test test_android_restore_backup
;;
openssl)
run_test test_android_openssl
;;
restore_backup)
run_test test_android_restore_backup
;;
*)
echo "Unknown --test value: $TEST_FILTER (use 'all', 'openssl', or 'restore_backup')"
exit 1
;;
esac
echo ""
echo "=== Done ==="