mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-10-25 04:27:19 +09:00
Compare commits
13
Commits
dev
...
feat/tests
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35e9833fa9 | ||
|
|
3d666c2014 | ||
|
|
a82e0ec5e4 | ||
|
|
12d9fcd712 | ||
|
|
24d13bacea | ||
|
|
4546815941 | ||
|
|
22fda3acc7 | ||
|
|
6b922c9d29 | ||
|
|
5ac34fd12f | ||
|
|
ed50553e17 | ||
|
|
25fcf68b4b | ||
|
|
830872cdbe | ||
|
|
5d7ef63c05 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
["1.1.1.1", "8.8.8.8", "9.9.9.9"]
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ==="
|
||||
Reference in new issue
Block a user