Compare commits

...
Author SHA1 Message Date
yp d3753a7e78 merge dev 2026-09-30 09:25:08 +03:00
vkamn 94b51df247 chore: bump version (#3187) 2026-09-18 20:44:38 +08:00
Mezooz de93650a90 fix: make Conan recipes parse on Python < 3.12 (#2870)
Three recipes use f-string syntax that is only valid on Python 3.12+
(PEP 701): reusing the same quote character inside an f-string
expression, and using a backslash inside one.

  recipes/amnezia-xray-bindings/conanfile.py:115
  recipes/tun2socks/conanfile.py:118
  recipes/hev-socks5-tunnel/conanfile.py:63-65

On older interpreters Conan fails to load the recipe with a SyntaxError
before resolving a single dependency, so the build cannot start:

  SyntaxError: unexpected character after line continuation character

This is not platform specific. It is invisible in CI only because
ubuntu-latest ships Python 3.12+. Debian 12 (bookworm), current stable
and the base for Raspberry Pi OS, ships Python 3.11.

Hoist the expressions out of the f-strings, and use single quotes for
the nested string literals. Behaviour is unchanged on every version.

Verified with python3 -m py_compile on Python 3.11.2 (aarch64,
Debian 12): all three fail before this change and pass after.
2026-09-18 13:59:14 +08:00
vkamn 448baad3e1 chore: bump version (#3185)
* chore: bump version

* feat: update translations
2026-09-18 13:52:33 +08:00
vkamn aca1b7dc5c feat: add format version for future config refactoring (#3184) 2026-09-18 13:31:07 +08:00
vkamn de36a81b58 chore: change android aqt host (#3182) 2026-09-17 13:43:35 +08:00
vkamn 76b81b43c1 chore: pin install-qt-action to v4.3.1 (#3180) 2026-09-16 22:24:34 +08:00
MrMirDan bea2f5f7db feat: clickable notification (#3178) 2026-09-16 22:22:51 +08:00
Yaroslav Gurov a4d38b5642 fix: windows certificate signing issues (#3175)
* fix: windows certificate signing issues

* fix: add has_ui option and phys/cloud opts
2026-09-16 22:22:19 +08:00
NickVs2015andMrMirDan b76c88bba3 feat: migrate test to main repo (#3172)
* basic tests  adaptation

* fix: ui servers model and controller test awg config

* fix: platforms test builds

* build dirs inspection

* fix: changed paths for tests and tests artifacts

* fix: 'deploy' parent dir

* windows ctest configuration and macos dependencies inspection

* windows check executable and macos copy openssl libs

* windows test executables check and macos copy openssl

* changed some path on windows

* android apk list

* fix: updated android apk path

* Verify Android test files

* fix: build tests only with AMNEZIA_BUILD_TESTS

* fix: copy openssl runtime next to test_openssl_library

* fix: awg2 test config and inverted description checks

* chore: move run_android_tests.sh to client/tests and drop personal paths

* chore: gate tests by option, scope secrets and drop global concurrency in ci

* chore: serialized vps test job, scoped secrets, backup secret guard, test data lookup

---------

Co-authored-by: MrMirDan <8mirdan8@gmail.com>
2026-09-16 17:05:43 +08:00
vkamn eab14fd957 chore: add define for app update (#3164) 2026-09-14 12:37:58 +08:00
SPTApyo 2a8242d306 feat: add French (fr_FR) localization (#3135)
* feat: add French (fr_FR) localization

* fix: align french catalogue with current telegram link
2026-09-09 21:44:51 +08:00
vkamn e60842bd9d chore: cleanup entitlements (#3149)
* chore: cleanup entitlements for macos ne

* chore: bump version
2026-09-09 21:12:03 +08:00
dranik 0d1cfbaf19 Fix for creating child elements for a parent element 2026-06-12 11:25:06 +03:00
92 changed files with 19036 additions and 2456 deletions

No files matched your search

+601 -10
View File
@@ -118,8 +118,16 @@ jobs:
shell: bash
env:
QT_INSTALL_DIR: ${{ runner.temp }}
AMNEZIA_BUILD_TESTS: ON
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:
@@ -242,10 +250,20 @@ jobs:
shell: cmd
env:
QT_INSTALL_DIR: ${{ runner.temp }}
AMNEZIA_BUILD_TESTS: ON
run: |
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:
@@ -504,7 +522,7 @@ jobs:
xcode-version: '26.3.0'
- name: 'Install Qt'
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: 'mac'
@@ -536,9 +554,17 @@ jobs:
NOTARYTOOL_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
NOTARYTOOL_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
AMNEZIA_BUILD_TESTS: ON
shell: bash
run: deploy/build.sh --generator Ninja --installer all
- 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:
@@ -792,7 +818,7 @@ jobs:
submodules: 'true'
- name: 'Install desktop Qt'
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
@@ -804,10 +830,10 @@ jobs:
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_x86_64 Qt'
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
host: 'all_os'
target: 'android'
arch: 'android_x86_64'
modules: ${{ env.QT_MODULES }}
@@ -816,10 +842,10 @@ jobs:
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_x86 Qt'
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
host: 'all_os'
target: 'android'
arch: 'android_x86'
modules: ${{ env.QT_MODULES }}
@@ -828,10 +854,10 @@ jobs:
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_armv7 Qt'
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
host: 'all_os'
target: 'android'
arch: 'android_armv7'
modules: ${{ env.QT_MODULES }}
@@ -840,10 +866,10 @@ jobs:
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Install android_arm64_v8a Qt'
uses: jurplel/install-qt-action@v4
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
host: 'linux'
host: 'all_os'
target: 'android'
arch: 'android_arm64_v8a'
modules: ${{ env.QT_MODULES }}
@@ -1003,3 +1029,568 @@ 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
QTEST_FUNCTION_TIMEOUT: 7200000
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 }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- name: Decode backup file
if: matrix.filter == 'BackupExportImportTest'
shell: bash
env:
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
run: |
if [ -z "$BACKUP_FILE_BASE64" ]; then
echo "::error::BACKUP_FILE_BASE64 secret is not set"
exit 1
fi
echo "$BACKUP_FILE_BASE64" | base64 --decode > "$RUNNER_TEMP/test.backup"
echo "BACKUP_PATH=$RUNNER_TEMP/test.backup" >> "$GITHUB_ENV"
- 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 \
-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="$PWD/deploy/build/client/tests:$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-vps:
name: Test self-hosted VPS
needs: Build-Linux-Ubuntu
# These tests install and remove docker containers on a single shared VPS:
# two runs at once corrupt each other's state, so only one job may touch it.
concurrency:
group: vps-tests
cancel-in-progress: false
runs-on: ubuntu-22.04
env:
QT_VERSION: 6.10.1
QTEST_FUNCTION_TIMEOUT: 7200000
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 }}
SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY: ${{ secrets.SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- 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 \
-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="$PWD/deploy/build/client/tests:$QT_ROOT/lib:${LD_LIBRARY_PATH:-}"
export QT_QPA_PLATFORM=offscreen
ctest --test-dir deploy/build/client/tests --output-on-failure -VV \
-R "^(SelfHostedServerSetupTest|MultipleExportsTest|AdminSelfHostedExportTest)$"
# ------------------------------------------------------
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
QTEST_FUNCTION_TIMEOUT: 7200000
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 }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
- name: Decode backup file
if: matrix.filter == 'BackupExportImportTest'
shell: pwsh
env:
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
run: |
if ([string]::IsNullOrWhiteSpace($env:BACKUP_FILE_BASE64)) {
Write-Output "::error::BACKUP_FILE_BASE64 secret is not set"
exit 1
}
$backup = Join-Path $env:RUNNER_TEMP "test.backup"
$bytes = [Convert]::FromBase64String($env: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
QTEST_FUNCTION_TIMEOUT: 7200000
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 }}
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: bash
env:
BACKUP_FILE_BASE64: ${{ secrets.BACKUP_FILE_BASE64 }}
run: |
if [ -z "$BACKUP_FILE_BASE64" ]; then
echo "::error::BACKUP_FILE_BASE64 secret is not set"
exit 1
fi
echo "$BACKUP_FILE_BASE64" | base64 --decode > "$RUNNER_TEMP/test.backup"
echo "BACKUP_PATH=$RUNNER_TEMP/test.backup" >> "$GITHUB_ENV"
- 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: Restore executable permissions
run: |
find deploy/build/client/tests \
-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:$QT_ROOT/lib:${DYLD_LIBRARY_PATH:-}"
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: 'all_os'
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 \
-DAMNEZIA_BUILD_TESTS=ON
- name: 'Build Android tests'
# env:
# QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
# QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
# QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
run: |
cmake --build build --target test_android_openssl_make_apk
cmake --build build --target test_android_restore_backup_make_apk
- name: 'Write Android test runner script'
run: |
cat > ci_run_android_tests.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
ADB=$(which adb)
echo ">>> Waiting for package manager to be ready..."
"$ADB" shell 'while ! pm list packages > /dev/null 2>&1; do sleep 2; done'
echo ">>> Package manager ready."
run_test() {
local target="$1"
local apk="build/client/tests/android-build/${target}.apk"
local pkg_name="org.qtproject.example.${target}"
local activity="${pkg_name}/org.qtproject.qt.android.bindings.QtActivity"
echo ">>> Installing ${target} APK..."
timeout 120 "$ADB" install -r "$apk"
"$ADB" shell "run-as $pkg_name sh -c 'mkdir -p files && : > files/stdout.txt'" 2>/dev/null || true
"$ADB" shell "run-as $pkg_name sh -c 'rm -f files/qtest_last_exit_code'" 2>/dev/null || true
echo ">>> Launching ${target}..."
"$ADB" shell am start -n "$activity"
echo ">>> Waiting for test to finish (up to 120s)..."
local elapsed=0 exit_code=""
while [[ $elapsed -lt 120 ]]; do
sleep 2
elapsed=$((elapsed + 2))
exit_code=$("$ADB" shell \
"run-as $pkg_name cat files/qtest_last_exit_code 2>/dev/null" \
| tr -d '[:space:]' || true)
[[ -n "$exit_code" ]] && break
done
local stdout_content
stdout_content=$("$ADB" shell \
"run-as $pkg_name cat files/stdout.txt 2>/dev/null" || true)
if [[ -n "$stdout_content" ]]; then
echo ">>> Test output:"
echo "$stdout_content"
fi
if [[ -z "$exit_code" ]]; then
echo ">>> TIMEOUT: ${target} did not complete within 120s."
exit 1
elif [[ "$exit_code" != "0" ]]; then
echo ">>> ${target} FAILED (exit code: $exit_code)"
exit 1
else
echo ">>> ${target} PASSED"
fi
}
run_test test_android_openssl
run_test test_android_restore_backup
EOF
chmod +x ci_run_android_tests.sh
- name: 'Run Android tests on emulator'
uses: reactivecircus/android-emulator-runner@v2
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
+4 -2
View File
@@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
if(NOT AMNEZIAVPN_VERSION)
set(AMNEZIAVPN_VERSION 5.0.2.1 CACHE STRING "Client app version")
set(AMNEZIAVPN_VERSION 5.0.3.1 CACHE STRING "Client app version")
endif()
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
@@ -20,6 +20,8 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
HOMEPAGE_URL "https://amnezia.org/"
)
option(AMNEZIA_BUILD_TESTS "Build test executables" OFF)
# trigger conan to kick off `conan install` globally
find_package(OpenSSL REQUIRED)
if (PREBUILTS_ONLY)
@@ -32,7 +34,7 @@ set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
# bump by 2 on every release, because we're releasing two versions on the play store
set(APP_ANDROID_VERSION_CODE 2161)
set(APP_ANDROID_VERSION_CODE 2171)
if(DEFINED APP_ANDROID_VERSION_CODE_OFFSET)
math(EXPR APP_ANDROID_VERSION_CODE "${APP_ANDROID_VERSION_CODE} + ${APP_ANDROID_VERSION_CODE_OFFSET}")
endif()
+5
View File
@@ -92,6 +92,7 @@ else()
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_hi_IN.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_es_ES.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ko_KR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_fr_FR.ts
)
endif()
@@ -313,3 +314,7 @@ if(OVPN_SCRIPTS)
WORLD_READ WORLD_EXECUTE
)
endif()
if(AMNEZIA_BUILD_TESTS)
add_subdirectory(tests)
endif()
+2
View File
@@ -179,7 +179,9 @@ void AmneziaApplication::init()
m_coreController->setQmlRoot();
#if CLIENT_ENABLE_APP_UPDATES
m_coreController->checkForAppUpdates();
#endif
#ifdef Q_OS_WIN //TODO
if (m_parser.isSet(m_optAutostart))
+4
View File
@@ -58,3 +58,7 @@ endif()
if(NOT CLIENT_NOTIFICATION_IDENTIFIER)
set(CLIENT_NOTIFICATION_IDENTIFIER "${CLIENT_TS_PREFIX}" CACHE STRING "Local notification request identifier")
endif()
if(NOT DEFINED CLIENT_ENABLE_APP_UPDATES)
set(CLIENT_ENABLE_APP_UPDATES 1)
endif()
+2
View File
@@ -22,6 +22,7 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/controllers/coreSignalHandlers.h
${CLIENT_ROOT_DIR}/core/controllers/gatewayController.h
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshSession.h
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshExecutor.h
${CLIENT_ROOT_DIR}/core/controllers/serversController.h
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/usersController.h
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/installController.h
@@ -107,6 +108,7 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/controllers/coreSignalHandlers.cpp
${CLIENT_ROOT_DIR}/core/controllers/gatewayController.cpp
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshSession.cpp
${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshExecutor.cpp
${CLIENT_ROOT_DIR}/core/controllers/serversController.cpp
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/usersController.cpp
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/installController.cpp
@@ -188,6 +188,9 @@ ErrorCode StorePurchaseController::importServiceFromMarket(const QString &userCo
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
if (!serverConfigUtils::isConfigFormatVersionSupported(configObject)) {
return ErrorCode::ConfigFormatVersionNotSupportedError;
}
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -239,6 +239,9 @@ ErrorCode SubscriptionController::applyImportedServiceConfig(const QString &user
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
if (!serverConfigUtils::isConfigFormatVersionSupported(serverConfigJson)) {
return ErrorCode::ConfigFormatVersionNotSupportedError;
}
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -291,6 +294,9 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
}
QJsonObject configObject = QJsonDocument::fromJson(configBytes).object();
if (!serverConfigUtils::isConfigFormatVersionSupported(configObject)) {
return ErrorCode::ConfigFormatVersionNotSupportedError;
}
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -380,6 +386,9 @@ ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serve
updateApiConfigInJson(serverConfigJson, apiV2->apiConfig.serviceType, serviceProtocol, apiV2->apiConfig.userCountryCode, responseBody);
if (!serverConfigUtils::isConfigFormatVersionSupported(serverConfigJson)) {
return ErrorCode::ConfigFormatVersionNotSupportedError;
}
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -1,9 +1,9 @@
#include "coreSignalHandlers.h"
#include <QTimer>
#include <QtConcurrent>
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/selfhosted/sshExecutor.h"
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/controllers/coreController.h"
@@ -145,7 +145,7 @@ void CoreSignalHandlers::initExportControllerHandler()
});
connect(m_coreController->m_exportController, &ExportController::revokeClientRequested, this,
[this](const QString &serverId, int row, DockerContainer container) {
QtConcurrent::run([this, serverId, row, container]() {
SshExecutor::instance().run(serverId, [this, serverId, row, container]() {
m_coreController->m_usersController->revokeClient(serverId, row, container);
});
});
@@ -205,8 +205,10 @@ void CoreSignalHandlers::initAdminConfigRevokedHandler()
{
connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this,
[this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) {
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
}, Qt::DirectConnection);
SshExecutor::instance().run(serverId, [this, serverId, containerConfig, container]() {
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
});
});
connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this,
[this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) {
@@ -33,6 +33,18 @@ using namespace ProtocolUtils;
namespace
{
bool rejectUnsupportedFormatVersion(ImportController::ImportResult &result)
{
if (serverConfigUtils::isConfigFormatVersionSupported(result.config)) {
return false;
}
qWarning() << "Config format version" << serverConfigUtils::configFormatVersion(result.config)
<< "is newer than supported" << serverConfigUtils::currentConfigFormatVersion;
result.errorCode = ErrorCode::ConfigFormatVersionNotSupportedError;
result.config = {};
return true;
}
ConfigTypes checkConfigFormat(const QString &config)
{
const QString openVpnConfigPatternCli = "client";
@@ -207,6 +219,10 @@ ImportController::ImportResult ImportController::extractConfigFromData(const QSt
case ConfigTypes::Amnezia: {
result.config = QJsonDocument::fromJson(config.toUtf8()).object();
if (rejectUnsupportedFormatVersion(result)) {
return result;
}
if (serverConfigUtils::isServerFromApi(result.config)) {
auto apiConfig = result.config.value(apiDefs::key::apiConfig).toObject();
apiConfig[apiDefs::key::vpnKey] = data;
@@ -256,6 +272,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
if (!dataObj.isEmpty()) {
result.config = dataObj;
result.configType = ConfigTypes::Amnezia;
rejectUnsupportedFormatVersion(result);
return result;
}
@@ -267,6 +284,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
return result;
}
result.configType = ConfigTypes::Amnezia;
rejectUnsupportedFormatVersion(result);
return result;
}
@@ -284,6 +302,7 @@ ImportController::ImportResult ImportController::extractConfigFromQr(const QByte
return result;
}
result.configType = ConfigTypes::Amnezia;
rejectUnsupportedFormatVersion(result);
return result;
}
@@ -379,6 +398,11 @@ int ImportController::qrChunksTotal() const
void ImportController::importConfig(const QJsonObject &config)
{
if (!serverConfigUtils::isConfigFormatVersionSupported(config)) {
emit importErrorOccurred(ErrorCode::ConfigFormatVersionNotSupportedError, false);
return;
}
ServerCredentials credentials;
credentials.hostName = config.value(configKey::hostName).toString();
credentials.port = config.value(configKey::port).toInt();
@@ -16,6 +16,7 @@
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/selfhosted/sshExecutor.h"
#include "core/installers/awgInstaller.h"
#include "core/installers/installerBase.h"
#include "core/installers/openvpnInstaller.h"
@@ -104,6 +105,7 @@ InstallController::~InstallController()
ErrorCode InstallController::setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config,
bool isUpdate)
{
qDebug().noquote() << "InstallController::setupContainer" << ContainerUtils::containerToString(container);
SshSession sshSession;
ErrorCode e = ErrorCode::NoError;
@@ -371,7 +373,7 @@ ErrorCode InstallController::validateAndPrepareConfig(const QString &serverId)
void InstallController::validateConfig(const QString &serverId)
{
QFuture<ErrorCode> future = QtConcurrent::run([this, serverId]() {
QFuture<ErrorCode> future = SshExecutor::instance().run(serverId, [this, serverId]() {
return validateAndPrepareConfig(serverId);
});
@@ -119,6 +119,11 @@ QByteArray SettingsController::backupAppConfig() const
return QJsonDocument(config).toJson();
}
int SettingsController::unsupportedFormatConfigsSkippedCount() const
{
return m_serversRepository->unsupportedFormatConfigsCount();
}
ErrorCode SettingsController::restoreAppConfigFromData(const QByteArray &data)
{
if (!m_appSettingsRepository->restoreAppConfig(data)) {
@@ -39,6 +39,7 @@ public:
QByteArray backupAppConfig() const;
ErrorCode restoreAppConfigFromData(const QByteArray &data);
int unsupportedFormatConfigsSkippedCount() const;
QString getAppVersion() const;
@@ -129,6 +129,10 @@ void UpdateController::setUpdateCheckRunning(bool running)
void UpdateController::checkForUpdates()
{
#if !CLIENT_ENABLE_APP_UPDATES
return;
#endif
if (m_updateCheckRunning || !m_appSettingsRepository) {
return;
}
@@ -85,6 +85,7 @@ QPair<QString, QString> ApiV2ServerConfig::getDnsPair(const QString &primaryDns,
QJsonObject ApiV2ServerConfig::toJson() const
{
QJsonObject obj;
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
if (!name.isEmpty()) {
obj[configKey::name] = name;
@@ -8,6 +8,7 @@
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/networkUtilities.h"
@@ -51,6 +52,7 @@ QPair<QString, QString> NativeServerConfig::getDnsPair(const QString &primaryDns
QJsonObject NativeServerConfig::toJson() const
{
QJsonObject obj;
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
@@ -9,6 +9,7 @@
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/serverConfigUtils.h"
namespace amnezia
{
@@ -83,6 +84,7 @@ QPair<QString, QString> SelfHostedAdminServerConfig::getDnsPair(bool isAmneziaDn
QJsonObject SelfHostedAdminServerConfig::toJson() const
{
QJsonObject obj;
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
@@ -4,6 +4,7 @@
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -66,6 +67,7 @@ QPair<QString, QString> SelfHostedUserServerConfig::getDnsPair(const QString &pr
QJsonObject SelfHostedUserServerConfig::toJson() const
{
QJsonObject obj;
obj[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
@@ -5,6 +5,7 @@
#include <QJsonValue>
#include <QSet>
#include <QUuid>
#include <QDebug>
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h"
@@ -142,6 +143,7 @@ void SecureServersRepository::persistDefaultServerFields()
void SecureServersRepository::loadFromStorage()
{
clearServerStateMaps();
m_unsupportedFormatConfigsCount = 0;
const QJsonArray serversArray =
QJsonDocument::fromJson(value(QStringLiteral("Servers/serversList"), QByteArray()).toByteArray())
@@ -152,6 +154,12 @@ void SecureServersRepository::loadFromStorage()
const QString candidateId = readStorageServerId(json);
const QString serverId = normalizedOrGeneratedServerId(candidateId);
const QJsonObject strippedJson = withoutStorageServerId(json);
if (!serverConfigUtils::isConfigFormatVersionSupported(strippedJson)) {
qWarning() << "Skipping stored server config with unsupported format version"
<< serverConfigUtils::configFormatVersion(strippedJson);
++m_unsupportedFormatConfigsCount;
continue;
}
const serverConfigUtils::ConfigType kind = serverConfigUtils::configTypeFromJson(strippedJson);
if (m_serverJsonById.contains(serverId) || kind == serverConfigUtils::ConfigType::Invalid) {
@@ -184,6 +192,11 @@ void SecureServersRepository::invalidateCache()
loadFromStorage();
}
int SecureServersRepository::unsupportedFormatConfigsCount() const
{
return m_unsupportedFormatConfigsCount;
}
void SecureServersRepository::clearServers()
{
clearServerStateMaps();
@@ -222,7 +235,8 @@ QString SecureServersRepository::addServer(const QString &serverId, const QJsonO
return id;
}
const QJsonObject strippedJson = withoutStorageServerId(serverJson);
if (serverConfigUtils::configTypeFromJson(strippedJson) != kind) {
if (!serverConfigUtils::isConfigFormatVersionSupported(strippedJson)
|| serverConfigUtils::configTypeFromJson(strippedJson) != kind) {
return id;
}
m_serverJsonById.insert(id, embedStorageServerId(id, strippedJson));
@@ -52,6 +52,8 @@ public:
void invalidateCache();
int unsupportedFormatConfigsCount() const;
signals:
void serverAdded(const QString &serverId);
void serverEdited(const QString &serverId);
@@ -77,6 +79,7 @@ private:
QVector<QString> m_orderedServerIds;
QString m_defaultServerId;
int m_unsupportedFormatConfigsCount = 0;
};
#endif // SECURESERVERSREPOSITORY_H
+1
View File
@@ -11,5 +11,6 @@
#define APP_IOS_BUNDLE_ID "@CLIENT_IOS_BUNDLE_ID@"
#define APP_IOS_STORE_URL_FALLBACK "@CLIENT_IOS_STORE_URL_FALLBACK@"
#define APP_ANDROID_PACKAGE "@CLIENT_ANDROID_PACKAGE@"
#define CLIENT_ENABLE_APP_UPDATES @CLIENT_ENABLE_APP_UPDATES@
#endif
+1
View File
@@ -25,6 +25,7 @@ namespace amnezia
constexpr QLatin1String accessToken("api_key");
constexpr QLatin1String config("config");
constexpr QLatin1String configVersion("config_version");
constexpr QLatin1String formatVersion("format_version");
constexpr QLatin1String sendPayload("send_payload");
constexpr QLatin1String sendPayloadEndpoint("endpoint");
+2
View File
@@ -82,6 +82,8 @@ namespace amnezia
RestoreBackupInvalidError = 904,
LegacyApiV1NotSupportedError = 905,
LegacyContainerNotSupportedError = 906,
ConfigFormatVersionNotSupportedError = 907,
RestoreBackupUnsupportedConfigsSkipped = 908,
// Android errors
AndroidError = 1000,
+2
View File
@@ -71,6 +71,8 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ImportBackupFileUseRestoreInstead): errorMessage = QObject::tr("Backup files cannot be imported here. Use 'Restore from backup' instead."); break;
case (ErrorCode::RestoreBackupInvalidError): errorMessage = QObject::tr("Backup file is corrupted or has invalid format"); break;
case (ErrorCode::LegacyApiV1NotSupportedError): errorMessage = QObject::tr("This legacy Amnezia subscription format is no longer supported"); break;
case (ErrorCode::ConfigFormatVersionNotSupportedError): errorMessage = QObject::tr("This configuration was created in a newer version of the application and is not fully supported. Please update the application"); break;
case (ErrorCode::RestoreBackupUnsupportedConfigsSkipped): errorMessage = QObject::tr("Some configurations from the backup were not restored because they require a newer version of the application"); break;
case (ErrorCode::LegacyContainerNotSupportedError): errorMessage = QObject::tr("This protocol is no longer supported. Please select another protocol or remove this container from the server settings."); break;
case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break;
case (ErrorCode::NoInstalledContainersError): errorMessage = QObject::tr("VPN Protocols is not installed.\n Please install VPN container at first"); break;
@@ -0,0 +1,33 @@
#include "sshExecutor.h"
SshExecutor &SshExecutor::instance()
{
static SshExecutor executor;
return executor;
}
SshExecutor::~SshExecutor()
{
QMutexLocker lock(&m_mutex);
for (QThreadPool *pool : std::as_const(m_pools)) {
pool->waitForDone();
delete pool;
}
m_pools.clear();
}
QThreadPool *SshExecutor::poolFor(const QString &serverId)
{
QMutexLocker lock(&m_mutex);
auto it = m_pools.find(serverId);
if (it != m_pools.end()) {
return it.value();
}
auto *pool = new QThreadPool();
pool->setMaxThreadCount(1); // serialize all SSH ops for this server
pool->setExpiryTimeout(-1); // keep the single worker thread alive between ops
m_pools.insert(serverId, pool);
return pool;
}
@@ -0,0 +1,47 @@
#ifndef SSHEXECUTOR_H
#define SSHEXECUTOR_H
#include <QHash>
#include <QMutex>
#include <QString>
#include <QThreadPool>
#include <QtConcurrent>
#include <utility>
// Per-server serial executor for long-running self-hosted SSH operations.
//
// All SSH work for a given serverId is queued onto a dedicated single-thread
// pool, so operations to the same server run strictly one at a time (no
// concurrent SSH sessions to one host => no races, and a natural guard against
// double-run). Operations to different servers still run in parallel.
//
// NOTE: do NOT route nested workers that are awaited from within an already
// queued operation (e.g. InstallController::isServerDpkgBusy) through the same
// per-server pool — the single worker thread would block waiting on itself.
// Such nested helpers must keep using the global QThreadPool.
class SshExecutor
{
public:
static SshExecutor &instance();
SshExecutor(const SshExecutor &) = delete;
SshExecutor &operator=(const SshExecutor &) = delete;
template <typename Functor>
auto run(const QString &serverId, Functor &&functor)
{
return QtConcurrent::run(poolFor(serverId), std::forward<Functor>(functor));
}
private:
SshExecutor() = default;
~SshExecutor();
QThreadPool *poolFor(const QString &serverId);
QHash<QString, QThreadPool *> m_pools;
QMutex m_mutex;
};
#endif // SSHEXECUTOR_H
+10
View File
@@ -34,6 +34,16 @@ bool hasThirdPartyConfig(const QJsonObject &json)
namespace serverConfigUtils
{
int configFormatVersion(const QJsonObject &serverConfigObject)
{
return serverConfigObject.value(amnezia::configKey::formatVersion).toInt(0);
}
bool isConfigFormatVersionSupported(const QJsonObject &serverConfigObject)
{
return configFormatVersion(serverConfigObject) <= currentConfigFormatVersion;
}
bool isServerFromApi(const QJsonObject &serverConfigObject)
{
const int configVersion = serverConfigObject.value(amnezia::configKey::configVersion).toInt();
+6
View File
@@ -25,6 +25,12 @@ enum ConfigSource {
AmneziaGateway
};
constexpr int currentConfigFormatVersion = 1;
int configFormatVersion(const QJsonObject &serverConfigObject);
bool isConfigFormatVersionSupported(const QJsonObject &serverConfigObject);
bool isServerFromApi(const QJsonObject &serverConfigObject);
ConfigSource getConfigSource(const QJsonObject &serverConfigObject);
-7
View File
@@ -6,15 +6,8 @@
<true/>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>app-proxy-provider</string>
<string>packet-tunnel-provider</string>
<string>dns-settings</string>
<string>relay</string>
<string>content-filter-provider</string>
<string>dns-proxy</string>
</array>
<key>com.apple.developer.system-extension.install</key>
<true/>
<key>com.apple.developer.networking.vpn.api</key>
<array>
<string>allow-vpn</string>
@@ -6,12 +6,7 @@
<true/>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>dns-settings</string>
<string>relay</string>
<string>packet-tunnel-provider</string>
<string>content-filter-provider</string>
<string>dns-proxy</string>
<string>app-proxy-provider</string>
</array>
<key>com.apple.developer.networking.vpn.api</key>
<array>
+417
View File
@@ -0,0 +1,417 @@
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_config_format_version
testConfigFormatVersion.cpp
)
target_link_libraries(test_config_format_version 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
)
# Tests run from a bare artifact directory on CI, so the OpenSSL runtime we
# compiled against must sit next to the binary (PATH / LD_LIBRARY_PATH /
# DYLD_LIBRARY_PATH point there). Otherwise the loader picks up the system
# library and the version checks in testOpenSSLLibrary fail.
foreach(_ssl_tgt OpenSSL::Crypto OpenSSL::SSL)
if(NOT TARGET "${_ssl_tgt}")
continue()
endif()
set(_ssl_loc "")
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")
set(_ssl_loc "${_candidate}")
break()
endif()
endforeach()
if(NOT _ssl_loc)
continue()
endif()
set(_ssl_files "")
if(WIN32)
if(_ssl_loc MATCHES "\\.dll$")
set(_ssl_files "${_ssl_loc}")
endif()
else()
get_filename_component(_ssl_dir "${_ssl_loc}" DIRECTORY)
get_filename_component(_ssl_name "${_ssl_loc}" NAME)
string(REGEX REPLACE "\\..*$" "" _ssl_stem "${_ssl_name}")
file(GLOB _ssl_files
"${_ssl_dir}/${_ssl_stem}.so*"
"${_ssl_dir}/${_ssl_stem}.dylib"
"${_ssl_dir}/${_ssl_stem}.*.dylib")
endif()
if(NOT _ssl_files)
message(STATUS "No shared runtime found for ${_ssl_tgt} (${_ssl_loc}) - skipping copy")
endif()
foreach(_ssl_file ${_ssl_files})
add_custom_command(TARGET test_openssl_library POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_ssl_file}" "$<TARGET_FILE_DIR:test_openssl_library>"
)
endforeach()
endforeach()
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 ConfigFormatVersionTest COMMAND test_config_format_version)
add_test(NAME UiLanguageModelAndControllerTest COMMAND test_ui_language_model_and_controller)
add_test(NAME UiIpModelAndControllerTest COMMAND test_ui_ip_model_and_controller)
add_test(NAME UiAppSTModelAndControllerTest COMMAND test_ui_app_st_model_and_controller)
add_test(NAME UiAllowedDnsModelAndControllerTest COMMAND test_ui_allowed_dns_model_and_controller)
add_test(NAME BackupExportImportTest COMMAND test_backup_export_import)
add_test(NAME ConnectionTest COMMAND test_connection)
add_test(NAME OpenSSLLibraryTest COMMAND test_openssl_library)
endif()
# Android tests are executed on-device via androidtestrunner in CI (see .github/workflows/tests.yml).
# CTest cannot run Android binaries on the host machine, so no add_test() entries are registered here.
@@ -0,0 +1,55 @@
<?xml version="1.0"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.qtproject.example"
android:installLocation="auto"
android:versionCode="-- %%INSERT_VERSION_CODE%% --"
android:versionName="-- %%INSERT_VERSION_NAME%% --">
<!-- %%INSERT_PERMISSIONS -->
<!-- %%INSERT_FEATURES -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true" />
<application
android:name="org.qtproject.qt.android.bindings.QtApplication"
android:hardwareAccelerated="false"
android:label="-- %%INSERT_APP_NAME%% --"
android:icon="-- %%INSERT_APP_ICON%% --"
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:fullBackupOnly="false">
<activity
android:name="org.qtproject.qt.android.bindings.QtActivity"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
android:label="-- %%INSERT_APP_NAME%% --"
android:launchMode="singleTop"
android:screenOrientation="unspecified"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="-- %%INSERT_APP_LIB_NAME%% --" />
<meta-data
android:name="android.app.arguments"
android:value="-- %%INSERT_APP_ARGUMENTS%% --" />
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.qtprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/qtprovider_paths"/>
</provider>
</application>
</manifest>
+20
View File
@@ -0,0 +1,20 @@
add_executable(test_ui_api_services_model_and_controller
testUiApiServicesModelAndController.cpp
)
target_link_libraries(test_ui_api_services_model_and_controller PRIVATE
Qt6::Test
test_common
)
add_executable(test_ui_news_model_and_controller
testUiNewsModelAndController.cpp
)
target_link_libraries(test_ui_news_model_and_controller PRIVATE
Qt6::Test
test_common
)
add_test(NAME UiApiServicesModelAndControllerTest COMMAND test_ui_api_services_model_and_controller)
add_test(NAME UiNewsModelAndControllerTest COMMAND test_ui_news_model_and_controller)
@@ -0,0 +1,97 @@
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QTest>
#include <QUuid>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
class TestUiApiServicesModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
// TODO: add env vars for api
private slots:
void initTestCase()
{
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase()
{
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init()
{
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testRolesAndSignals()
{
QSignalSpy errorOccurredSpy(m_coreController->m_servicesCatalogUiController, &ServicesCatalogUiController::errorOccurred);
/* TODO:
m_coreController->m_servicesCatalogUiController->fillAvailableServices();
QVERIFY(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QModelIndex serviceModelIndex = m_coreController->m_apiServicesModel->index(0, 0);
QVERIFY2(serviceModelIndex.isValid(), "Service model index should be valid");
auto serviceName = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::NameRole);
QCOMPARE(serviceName, );
auto serviceCardDescription = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::CardDescriptionRole);
QCOMPARE(serviceCardDescription, );
auto isServiceAvailable = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::IsServiceAvailableRole);
QCOMPARE(isServiceAvailable, );
auto serviceSpeed = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::SpeedRole);
QCOMPARE(serviceSpeed, );
auto serviceTimeLimit = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::TimeLimitRole);
QCOMPARE(serviceTimeLimit, );
auto serviceRegion = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::RegionRole);
QCOMPARE(serviceRegion, );
auto serviceFeatures = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::FeaturesRole);
QCOMPARE(serviceFeatures, );
auto servicePrice = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::PriceRole);
QCOMPARE(servicePrice, );
auto serviceEndDate = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::EndDateRole);
QCOMPARE(serviceEndDate, );
auto serviceOrder = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::OrderRole);
QCOMPARE(serviceOrder, );
*/
}
};
QTEST_MAIN(TestUiApiServicesModelAndController)
#include "testUiApiServicesModelAndController.moc"
@@ -0,0 +1,97 @@
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QTest>
#include <QUuid>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
class TestUiNewsModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
// TODO: add env vars for api
private slots:
void initTestCase()
{
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase()
{
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init()
{
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testRolesAndSignals()
{
QSignalSpy fetchNewsFinishedSpy(m_coreController->m_apiNewsUiController, &ApiNewsUiController::fetchNewsFinished);
QSignalSpy errorOccurredSpy(m_coreController->m_apiNewsUiController, &ApiNewsUiController::errorOccurred);
QSignalSpy processedIndexChangedSpy(m_coreController->m_newsModel, &NewsModel::processedIndexChanged);
QSignalSpy hasUnreadChangedSpy(m_coreController->m_newsModel, &NewsModel::hasUnreadChanged);
/* TODO:
m_coreController->m_apiNewsUiController->fetchNews(false);
QVERIFY(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QVERIFY(fetchNewsFinishedSpy.count() == 1, "fetchNewsFinished signal should be emitted");
m_coreController->m_newsModel->updateModel();
QVERIFY(hasUnreadChangedSpy.count() == 1, "hasUnreadChanged signal should be emitted");
QModelIndex newsModelIndex = m_coreController->m_newsModel->index(0, 0);
QVERIFY2(newsModelIndex.isValid(), "News model index should be valid");
auto newsId = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::IdRole);
QCOMPARE(newsId, );
auto newsTitle = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::TitleRole);
QCOMPARE(newsTitle, );
auto newsContent = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::ContentRole);
QCOMPARE(newsContent, );
auto newsTimestamp = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::TimestampRole);
QCOMPARE(newsTimestamp, );
auto newsIsRead = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::IsReadRole);
QCOMPARE(newsIsRead, false);
auto newsIsProcessed = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::IsProcessedRole);
QCOMPARE(newsIsProcessed, );
m_coreController->m_newsModel->markAsRead(0);
? m_coreController->m_newsModel->updateModel(); ?
QVERIFY(hasUnreadChangedSpy.count() == 2, "hasUnreadChanged signal should be emitted");
QCOMPARE(newsIsRead, true);
*/
}
};
QTEST_MAIN(TestUiNewsModelAndController)
#include "testUiNewsModelAndController.moc"
+1
View File
@@ -0,0 +1 @@
["1.1.1.1", "8.8.8.8", "9.9.9.9"]
+1
View File
@@ -0,0 +1 @@
[{"hostname": "1.2.3.4"}, {"hostname": "5.6.7.8"}, {"hostname": "10.20.30.40"}]
+279
View File
@@ -0,0 +1,279 @@
#!/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_ROOT points to a Qt installation with desktop + Android kits (e.g. ~/Qt/6.10.1)
# - ANDROID_HOME (or ANDROID_SDK_ROOT) points to an Android SDK with NDK $NDK_VERSION
# - cmake, ninja, adb, python3 in PATH
# - `adb devices` shows at least one device/emulator
# - Conan 2 installed (pip install "conan==2.28.0")
#
# Usage (from the repository root):
# bash client/tests/run_android_tests.sh # x86_64 emulator (default)
# bash client/tests/run_android_tests.sh --abi arm64-v8a # physical ARM device
# bash client/tests/run_android_tests.sh --test openssl # only openssl test
# bash client/tests/run_android_tests.sh --test restore_backup # only restore-backup test
# bash client/tests/run_android_tests.sh --wait-boot # wait for emulator to boot first
set -euo pipefail
# ── Configurable defaults ──────────────────────────────────────────────────────
QT_ROOT="${QT_ROOT:?set QT_ROOT to your Qt installation, e.g. ~/Qt/6.10.1}"
ANDROID_SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:?set ANDROID_HOME to your 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"
case "$(uname -s)" in
Darwin) QT_HOST="$QT_ROOT/macos" ;;
Linux) QT_HOST="$QT_ROOT/gcc_64" ;;
*) echo "Unsupported host: $(uname -s)"; exit 1 ;;
esac
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 client/tests/run_android_tests.sh --list-devices"
echo " Then: bash client/tests/run_android_tests.sh --device <serial>"
exit 1
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$REPO_ROOT"
command -v cmake >/dev/null || { echo "ERROR: cmake not found in PATH"; exit 1; }
command -v ninja >/dev/null || { echo "ERROR: ninja not found in PATH"; exit 1; }
# Detect the 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"
# ── Add NDK toolchain to PATH (provides clang / clang++) ─────────────────────
export PATH="$NDK_PATH/toolchains/llvm/prebuilt/$NDK_HOST_TAG/bin:$PATH"
# ── 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; }
# 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" \
-DAMNEZIA_BUILD_TESTS=ON
# Patch deployment settings JSONs: cmake may leave ndk-host empty on Apple Silicon
for json_file in "$BUILD_DIR"/client/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/client/tests/android-build"
local apk="$apk_dir/${target}.apk"
local pkg_name="org.qtproject.example.${target}"
local activity="${pkg_name}/org.qtproject.qt.android.bindings.QtActivity"
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 ==="
@@ -0,0 +1,31 @@
add_executable(test_admin_self_hosted_export
testAdminSelfHostedExport.cpp
)
target_link_libraries(test_admin_self_hosted_export PRIVATE
Qt6::Test
test_common
)
add_executable(test_self_hosted_server_setup
testSelfHostedServerSetup.cpp
)
target_link_libraries(test_self_hosted_server_setup PRIVATE
Qt6::Test
test_common
)
add_executable(test_multiple_exports
testMultipleExports.cpp
)
target_link_libraries(test_multiple_exports PRIVATE
Qt6::Test
test_common
)
add_test(NAME AdminSelfHostedExportTest COMMAND test_admin_self_hosted_export)
add_test(NAME SelfHostedServerSetupTest COMMAND test_self_hosted_server_setup)
add_test(NAME MultipleExportsTest COMMAND test_multiple_exports)
@@ -0,0 +1,206 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QFile>
#include <QDebug>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/utils/constants/configKeys.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia::test;
class TestAdminSelfHostedExport : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
QJsonObject decodeVpnKey(const QString &vpnKey) {
QString key = vpnKey;
key.replace("vpn://", "");
QByteArray ba = QByteArray::fromBase64(
key.toUtf8(),
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals
);
qDebug() << "Base64 decoded size:" << ba.size();
QJsonDocument testDoc = QJsonDocument::fromJson(ba);
if (!testDoc.isNull()) {
qDebug() << "Data is not compressed, using as-is";
return testDoc.object();
}
QByteArray baUncompressed = qUncompress(ba);
if (!baUncompressed.isEmpty()) {
qDebug() << "Data was compressed, uncompressed size:" << baUncompressed.size();
ba = baUncompressed;
} else {
qDebug() << "qUncompress failed or data is not compressed";
}
return QJsonDocument::fromJson(ba).object();
}
// Checks that every value present in `expected` exists in `actual` unchanged
// (recursively). Extra keys in `actual` are allowed: a newer client adds
// default fields on export (e.g. xray xhttp/mkcp) that are absent in a key
// generated by an older version. Losing or changing imported data still fails.
static bool isJsonSubset(const QJsonValue &expected, const QJsonValue &actual, QString &path) {
if (expected.isObject()) {
if (!actual.isObject()) {
path += " (not an object)";
return false;
}
const QJsonObject expObj = expected.toObject();
const QJsonObject actObj = actual.toObject();
for (auto it = expObj.constBegin(); it != expObj.constEnd(); ++it) {
QString subPath = path + "/" + it.key();
if (!actObj.contains(it.key())) {
path = subPath + " (missing)";
return false;
}
if (!isJsonSubset(it.value(), actObj.value(it.key()), subPath)) {
path = subPath;
return false;
}
}
return true;
}
if (expected.isArray()) {
if (!actual.isArray()) {
path += " (not an array)";
return false;
}
const QJsonArray expArr = expected.toArray();
const QJsonArray actArr = actual.toArray();
if (expArr.size() != actArr.size()) {
path += QString(" (array size %1 != %2)").arg(expArr.size()).arg(actArr.size());
return false;
}
for (int i = 0; i < expArr.size(); ++i) {
QString subPath = path + QString("/[%1]").arg(i);
if (!isJsonSubset(expArr.at(i), actArr.at(i), subPath)) {
path = subPath;
return false;
}
}
return true;
}
if (expected != actual) {
path += " (value differs)";
return false;
}
return true;
}
QJsonObject sortContainers(const QJsonObject &config) {
QJsonObject sorted = config;
if (!config.contains("containers")) {
return sorted;
}
QJsonArray containers = config["containers"].toArray();
QVector<QJsonObject> containerVec;
for (const QJsonValue &val : containers) {
containerVec.append(val.toObject());
}
std::sort(containerVec.begin(), containerVec.end(), [](const QJsonObject &a, const QJsonObject &b) {
return a["container"].toString() < b["container"].toString();
});
QJsonArray sortedContainers;
for (const QJsonObject &obj : containerVec) {
sortedContainers.append(obj);
}
sorted["containers"] = sortedContainers;
return sorted;
}
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
}
void testAdminSelfHostedExport() {
QString vpnKey = getEnvValue("SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY");
logEnvValueState("SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY");
if (!isEnvValueConfigured(vpnKey)) {
QSKIP("Set SELF_HOSTED_ADMIN_EXPORT_ROUND_TRIP_VPN_KEY");
}
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(vpnKey);
QVERIFY2(importResult.errorCode == ErrorCode::NoError, "Import should succeed");
QVERIFY2(!importResult.config.isEmpty(), "Config should not be empty");
QJsonObject importedConfig = importResult.config;
m_coreController->m_importCoreController->importConfig(importedConfig);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
const QString serverId = m_coreController->m_serversRepository->defaultServerId();
auto exportResult = m_coreController->m_exportController->generateFullAccessConfig(serverId);
QVERIFY2(exportResult.errorCode == ErrorCode::NoError, "Export should succeed");
QVERIFY2(!exportResult.config.isEmpty(), "Exported config should not be empty");
qDebug() << "EXPORTED KEY:" << exportResult.config;
QJsonObject exportedConfig = decodeVpnKey(exportResult.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(exportResult.config);
QVERIFY2(importResult2.errorCode == ErrorCode::NoError, "Re-import should succeed");
QJsonObject sortedImported = sortContainers(importedConfig);
QJsonObject sortedExported = sortContainers(importResult2.config);
QString importedJson = QJsonDocument(sortedImported).toJson(QJsonDocument::Compact);
QString exportedJson = QJsonDocument(sortedExported).toJson(QJsonDocument::Compact);
qDebug() << "IMPORTED JSON:" << importedJson;
qDebug() << "EXPORTED JSON:" << exportedJson;
QString divergedPath;
QVERIFY2(isJsonSubset(sortedImported, sortedExported, divergedPath),
qPrintable("Exported config lost or changed imported data at: " + divergedPath));
}
};
QTEST_MAIN(TestAdminSelfHostedExport)
#include "testAdminSelfHostedExport.moc"
@@ -0,0 +1,266 @@
#include <QDebug>
#include <QUuid>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/commonStructs.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include <libssh/libssh.h>
#include "secureQSettings.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
using namespace amnezia;
using namespace amnezia::test;
namespace {
enum class NativeExportKind {
Awg = 1,
WireGuard,
OpenVpn,
Xray,
};
QList<DockerContainer> vpnContainersForExport()
{
return {
DockerContainer::Awg2,
DockerContainer::WireGuard,
DockerContainer::OpenVpn,
DockerContainer::Xray
};
}
} // namespace
class TestMultipleExports : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
QString m_serverId;
ServerCredentials getCredentialsFromIni()
{
ServerCredentials credentials;
credentials.hostName = getEnvValue("SELF_HOSTED_SERVER_HOST_NAME");
credentials.userName = getEnvValue("SELF_HOSTED_SERVER_USER_NAME");
credentials.secretData = getEnvValue("SELF_HOSTED_SERVER_PASSWORD");
credentials.port = getEnvValue("SELF_HOSTED_SERVER_SSH_PORT").toInt();
logEnvValueState("SELF_HOSTED_SERVER_HOST_NAME");
logEnvValueState("SELF_HOSTED_SERVER_USER_NAME");
logEnvValueState("SELF_HOSTED_SERVER_PASSWORD");
logEnvValueState("SELF_HOSTED_SERVER_SSH_PORT");
return credentials;
}
int portForContainer(DockerContainer container) const
{
const Proto proto = ContainerUtils::defaultProtocol(container);
if (container == DockerContainer::Awg2) {
return ProtocolUtils::getPortForInstall(proto);
}
if (container == DockerContainer::Xray) {
// The default 443 is occupied on the test VPS
// (installContainer fails with ServerPortAlreadyAllocatedError).
return 8443;
}
return ProtocolUtils::defaultPort(proto);
}
TransportProto transportForContainer(DockerContainer container) const
{
return ProtocolUtils::defaultTransportProto(ContainerUtils::defaultProtocol(container));
}
void installVpnContainer(DockerContainer container)
{
const QString containerName = ContainerUtils::containerToString(container);
bool wasInstalled = false;
const ErrorCode error = m_coreController->m_installController->installContainer(
m_serverId, container, portForContainer(container), transportForContainer(container), wasInstalled);
QVERIFY2(error == ErrorCode::NoError,
QString("%1: installContainer should succeed. Error: %2")
.arg(containerName)
.arg(static_cast<int>(error))
.toUtf8()
.constData());
qDebug() << containerName << "installed:" << wasInstalled;
}
void setupServerWithVpnContainers()
{
ServerCredentials credentials = getCredentialsFromIni();
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
QSKIP("Set SELF_HOSTED_SERVER_HOST_NAME, SELF_HOSTED_SERVER_USER_NAME, SELF_HOSTED_SERVER_PASSWORD");
}
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName
<< "port:" << credentials.port;
QString sshOutput;
const ErrorCode sshError =
m_coreController->m_installController->checkSshConnection(credentials, sshOutput);
QVERIFY2(sshError == ErrorCode::NoError,
QString("SSH connection should succeed. Error: %1, Output: %2")
.arg(static_cast<int>(sshError))
.arg(sshOutput)
.toUtf8()
.constData());
m_coreController->m_installController->addEmptyServer(credentials);
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
m_serverId = m_coreController->m_serversRepository->defaultServerId();
QVERIFY2(!m_serverId.isEmpty(), "Server id should not be empty");
const ErrorCode removeError = m_coreController->m_installController->removeAllContainers(m_serverId);
QVERIFY2(removeError == ErrorCode::NoError,
QString("removeAllContainers should succeed. Error: %1")
.arg(static_cast<int>(removeError))
.toUtf8()
.constData());
qDebug() << "Remote Amnezia services cleared";
for (DockerContainer container : vpnContainersForExport()) {
installVpnContainer(container);
}
qDebug() << "All VPN containers installed for export tests";
}
void skipIfContainerNotInstalled(const QString &containerName, int containerIndex)
{
if (!m_coreController->m_containersModel->data(containerIndex, ContainersModel::Roles::IsInstalledRole).toBool()) {
const QString reason = QString("%1: Not installed").arg(containerName);
QSKIP(reason.toUtf8().constData());
}
}
private slots:
void initTestCase()
{
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
ssh_init();
setupServerWithVpnContainers();
}
void cleanupTestCase()
{
if (!m_serverId.isEmpty()) {
m_coreController->m_installController->removeAllContainers(m_serverId);
/* Raises critical error because cant clear clientTable
for (int containerIndex = 1; containerIndex < 7; ++containerIndex) {
m_coreController->m_installUiController->clearCachedProfile(m_serverId, containerIndex);
}
*/
m_coreController->m_serversController->removeServer(m_serverId);
qDebug() << "SERVER REMOVED\n";
}
ssh_finalize();
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void testMultipleExports_data()
{
QTest::addColumn<QString>("containerName");
QTest::addColumn<int>("containerIndex");
QTest::newRow("awg2") << "Awg2" << static_cast<int>(DockerContainer::Awg2);
QTest::newRow("wireguard") << "WireGuard" << static_cast<int>(DockerContainer::WireGuard);
QTest::newRow("openvpn") << "OpenVPN" << static_cast<int>(DockerContainer::OpenVpn);
QTest::newRow("xray") << "XRay" << static_cast<int>(DockerContainer::Xray);
}
void testMultipleExports()
{
QFETCH(QString, containerName);
QFETCH(int, containerIndex);
skipIfContainerNotInstalled(containerName, containerIndex);
const QString clientName = "MultipleExports Test Client";
const auto exportResult =
m_coreController->m_exportController->generateConnectionConfig(m_serverId, containerIndex, clientName);
QVERIFY2(exportResult.errorCode == ErrorCode::NoError,
QString("\n%1: Export should succeed").arg(containerName).toUtf8().constData());
QVERIFY2(!exportResult.config.isEmpty(),
QString("%1: Exported config should not be empty").arg(containerName).toUtf8().constData());
}
void testMultipleExportsNative_data()
{
QTest::addColumn<QString>("containerName");
QTest::addColumn<int>("containerIndex");
QTest::addColumn<int>("nativeExportKind");
QTest::newRow("awg") << "Awg2" << static_cast<int>(DockerContainer::Awg2)
<< static_cast<int>(NativeExportKind::Awg);
QTest::newRow("wireguard") << "WireGuard" << static_cast<int>(DockerContainer::WireGuard)
<< static_cast<int>(NativeExportKind::WireGuard);
QTest::newRow("openvpn") << "OpenVPN" << static_cast<int>(DockerContainer::OpenVpn)
<< static_cast<int>(NativeExportKind::OpenVpn);
QTest::newRow("xray") << "XRay" << static_cast<int>(DockerContainer::Xray)
<< static_cast<int>(NativeExportKind::Xray);
}
void testMultipleExportsNative()
{
QFETCH(QString, containerName);
QFETCH(int, containerIndex);
QFETCH(int, nativeExportKind);
skipIfContainerNotInstalled(containerName, containerIndex);
const QString clientName = "MultipleExports Test Client";
ExportController::ExportResult exportResult;
switch (static_cast<NativeExportKind>(nativeExportKind)) {
case NativeExportKind::Awg:
exportResult = m_coreController->m_exportController->generateAwgConfig(m_serverId, containerIndex, clientName);
break;
case NativeExportKind::WireGuard:
exportResult = m_coreController->m_exportController->generateWireGuardConfig(m_serverId, clientName);
break;
case NativeExportKind::OpenVpn:
exportResult = m_coreController->m_exportController->generateOpenVpnConfig(m_serverId, clientName);
break;
case NativeExportKind::Xray:
exportResult = m_coreController->m_exportController->generateXrayConfig(m_serverId, clientName);
break;
}
QVERIFY2(exportResult.errorCode == ErrorCode::NoError,
QString("\n%1 (native): Export should succeed").arg(containerName).toUtf8().constData());
QVERIFY2(!exportResult.config.isEmpty(),
QString("%1 (native): Exported config should not be empty").arg(containerName).toUtf8().constData());
}
};
QTEST_MAIN(TestMultipleExports)
#include "testMultipleExports.moc"
@@ -0,0 +1,397 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QProcessEnvironment>
#include <QDebug>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/awgProtocolConfig.h"
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/utils/commonStructs.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/errorCodes.h"
#include "ui/models/serversModel.h"
#include <libssh/libssh.h>
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestSelfHostedServerSetup : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
ServerCredentials getCredentialsFromEnv() {
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
QString hostName = getEnvValue("SELF_HOSTED_SERVER_HOST_NAME");
QString userName = getEnvValue("SELF_HOSTED_SERVER_USER_NAME");
QString password = getEnvValue("SELF_HOSTED_SERVER_PASSWORD");
QString portStr = getEnvValue("SELF_HOSTED_SERVER_SSH_PORT");
int port = portStr.toInt();
logEnvValueState("SELF_HOSTED_SERVER_HOST_NAME");
logEnvValueState("SELF_HOSTED_SERVER_USER_NAME");
logEnvValueState("SELF_HOSTED_SERVER_PASSWORD");
logEnvValueState("SELF_HOSTED_SERVER_SSH_PORT");
ServerCredentials credentials;
credentials.hostName = hostName;
credentials.userName = userName;
credentials.secretData = password;
credentials.port = port;
return credentials;
}
void verifySshConnection(ServerCredentials credentials) {
QString sshOutput;
ErrorCode sshError = m_coreController->m_installController->checkSshConnection(credentials, sshOutput);
QVERIFY2(sshError == ErrorCode::NoError,
QString("SSH connection should succeed. Error: %1, Output: %2")
.arg(static_cast<int>(sshError))
.arg(sshOutput)
.toUtf8().constData());
qDebug() << "SSH connection successful. Output:" << sshOutput;
}
void verifyAdminAccess(int serverIndex)
{
const QString serverId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
const auto adminCfg = m_coreController->m_serversRepository->selfHostedAdminConfig(serverId);
QVERIFY2(adminCfg.has_value(), "Server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig &selfHosted = *adminCfg;
QVERIFY2(selfHosted.hasCredentials(),
"Server should have credentials (admin access)");
QVERIFY2(!selfHosted.userName.isEmpty(),
"Server should have userName for admin access");
QVERIFY2(!selfHosted.password.isEmpty(),
"Server should have password for admin access");
QVERIFY2(!selfHosted.isReadOnly(),
"Server should not be read-only (should have admin access)");
if (m_coreController->m_serversModel) {
bool hasWriteAccess = m_coreController->m_serversModel->data(
m_coreController->m_serversModel->index(serverIndex, 0),
ServersModel::HasWriteAccessRole
).toBool();
QVERIFY2(hasWriteAccess,
"Server should have write access (admin access) according to ServersModel");
}
qDebug() << "Admin access verified for server at index:" << serverIndex;
}
void verifyClientConfig(const ContainerConfig& containerConfig, DockerContainer container) {
QString containerName = ContainerUtils::containerToString(container);
qDebug() << "Checking container:" << containerName;
if (ContainerUtils::containerService(container) != ServiceType::Other) {
bool hasClientConfig = containerConfig.protocolConfig.hasClientConfig();
QVERIFY2(hasClientConfig,
QString("Container %1 should have client config initialized")
.arg(containerName)
.toUtf8().constData());
if (container == DockerContainer::Awg) {
const AwgProtocolConfig* awgProtocolConfig = containerConfig.protocolConfig.as<AwgProtocolConfig>();
QVERIFY2(awgProtocolConfig != nullptr, "Protocol config should be AwgProtocolConfig");
QVERIFY2(awgProtocolConfig->hasClientConfig(), "AwgProtocolConfig should have client config");
const std::optional<AwgClientConfig>& clientCfgOpt = awgProtocolConfig->clientConfig;
QVERIFY2(clientCfgOpt.has_value(), "Awg client config should exist");
const AwgClientConfig& awgClientConfig = *clientCfgOpt;
QVERIFY2(!awgClientConfig.hostName.isEmpty(), "Awg client config should have hostName");
QVERIFY2(awgClientConfig.port > 0, "Awg client config should have valid port");
QVERIFY2(!awgClientConfig.clientPrivateKey.isEmpty(), "Awg client config should have clientPrivateKey");
QVERIFY2(!awgClientConfig.clientPublicKey.isEmpty(), "Awg client config should have clientPublicKey");
QVERIFY2(!awgClientConfig.serverPublicKey.isEmpty(), "Awg client config should have serverPublicKey");
QVERIFY2(!awgClientConfig.clientId.isEmpty(), "Awg client config should have clientId");
QVERIFY2(!awgClientConfig.nativeConfig.isEmpty(), "Awg client config should have nativeConfig");
}
qDebug() << "Container" << containerName << "has valid client config initialized";
} else {
qDebug() << "Container" << containerName << "is service type Other, skipping client config check";
}
}
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
ssh_init();
}
void cleanupTestCase() {
ssh_finalize();
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testSelfHostedServerSetup() {
ServerCredentials credentials = getCredentialsFromEnv();
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
QSKIP("Set SELF_HOSTED_SERVER_HOST_NAME, SELF_HOSTED_SERVER_USER_NAME, SELF_HOSTED_SERVER_PASSWORD");
}
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
verifySshConnection(credentials);
int awgPort = 55424;
TransportProto awgTransportProto = TransportProto::Udp;
bool wasAwgInstalled = false;
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
ErrorCode installServerError = m_coreController->m_installController->installServer(
credentials, DockerContainer::Awg, awgPort, awgTransportProto, wasAwgInstalled);
QVERIFY2(installServerError == ErrorCode::NoError,
QString("installServer for Awg should succeed. Error: %1")
.arg(static_cast<int>(installServerError))
.toUtf8().constData());
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
qDebug() << "Server with Awg container added at index:" << serverIndex;
const auto adminAfterAwg = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(adminAfterAwg.has_value(), "Server should be self-hosted (admin)");
const SelfHostedAdminServerConfig *selfHostedAfterAwg = &(*adminAfterAwg);
QVERIFY2(selfHostedAfterAwg->defaultContainer == DockerContainer::Awg, "Default container should be Awg");
QVERIFY2(selfHostedAfterAwg->containers.contains(DockerContainer::Awg), "Server should have Awg container");
ContainerConfig awgConfig = selfHostedAfterAwg->containers.value(DockerContainer::Awg);
QVERIFY2(awgConfig.container == DockerContainer::Awg, "Awg container config should be valid");
QVERIFY2(selfHostedAfterAwg->containers.size() == 1,
QString("Server should have exactly 1 container after Awg installation, but has %1")
.arg(selfHostedAfterAwg->containers.size())
.toUtf8().constData());
verifyClientConfig(awgConfig, DockerContainer::Awg);
qDebug() << "Awg container installed and configured successfully with valid client config";
int dnsPort = 53;
TransportProto dnsTransportProto = TransportProto::Udp;
bool wasDnsInstalled = false;
const QString serverIdForOps = m_coreController->m_serversRepository->serverIdAt(serverIndex);
ErrorCode installContainerError = m_coreController->m_installController->installContainer(
serverIdForOps, DockerContainer::Dns, dnsPort, dnsTransportProto, wasDnsInstalled);
QVERIFY2(installContainerError == ErrorCode::NoError,
QString("installContainer for Dns should succeed. Error: %1")
.arg(static_cast<int>(installContainerError))
.toUtf8().constData());
qDebug() << "Dns container installed:" << wasDnsInstalled;
const auto adminAfterDns = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(adminAfterDns.has_value(), "Server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig *selfHostedAfterDns = &(*adminAfterDns);
QVERIFY2(selfHostedAfterDns->containers.contains(DockerContainer::Awg), "Server should still have Awg container");
QVERIFY2(selfHostedAfterDns->containers.contains(DockerContainer::Dns), "Server should have Dns container");
QVERIFY2(selfHostedAfterDns->containers.size() == 2,
QString("Server should have exactly 2 containers after Dns installation, but has %1")
.arg(selfHostedAfterDns->containers.size())
.toUtf8().constData());
ContainerConfig dnsConfig = selfHostedAfterDns->containers.value(DockerContainer::Dns);
QVERIFY2(dnsConfig.container == DockerContainer::Dns, "Dns container config should be valid");
const DnsProtocolConfig* dnsProtocolConfig = dnsConfig.protocolConfig.as<DnsProtocolConfig>();
QVERIFY2(dnsProtocolConfig != nullptr, "Protocol config should be DnsProtocolConfig");
qDebug() << "Dns container installed and configured successfully";
verifyAdminAccess(serverIndex);
qDebug() << "Test completed successfully. Server setup with Awg and Dns containers is complete.";
}
void testSelfHostedServerEmptyRecover() {
ServerCredentials credentials = getCredentialsFromEnv();
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
QSKIP("Set SELF_HOSTED_SERVER_HOST_NAME, SELF_HOSTED_SERVER_USER_NAME, SELF_HOSTED_SERVER_PASSWORD");
}
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
verifySshConnection(credentials);
SelfHostedAdminServerConfig serverConfig;
serverConfig.hostName = credentials.hostName;
serverConfig.userName = credentials.userName;
serverConfig.password = credentials.secretData;
serverConfig.port = credentials.port;
serverConfig.description = m_coreController->m_serversRepository->nextAvailableServerName();
serverConfig.displayName = serverConfig.description.isEmpty() ? serverConfig.hostName : serverConfig.description;
serverConfig.defaultContainer = DockerContainer::None;
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
m_coreController->m_serversRepository->addServer(QString(), serverConfig.toJson(),
serverConfigUtils::ConfigType::SelfHostedAdmin);
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() > 0, "Server should be added");
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
qDebug() << "Empty server added at index:" << serverIndex;
const auto addedAdmin = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(addedAdmin.has_value(), "Added server should be self-hosted admin");
const SelfHostedAdminServerConfig *selfHosted = &(*addedAdmin);
QVERIFY2(selfHosted->containers.isEmpty(), "Server should have no containers initially");
QVERIFY2(selfHosted->defaultContainer == DockerContainer::None, "Default container should be None");
const QString scanServerId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
ErrorCode scanError = m_coreController->m_installController->scanServerForInstalledContainers(scanServerId);
QVERIFY2(scanError == ErrorCode::NoError,
QString("Server scan should succeed. Error: %1")
.arg(static_cast<int>(scanError))
.toUtf8().constData());
qDebug() << "Server scan completed successfully";
const auto scannedAdmin = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(scannedAdmin.has_value(), "Scanned server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig *scannedSelfHosted = &(*scannedAdmin);
QMap<DockerContainer, ContainerConfig> containers = scannedSelfHosted->containers;
int containersCount = containers.size();
qDebug() << "Found containers count:" << containersCount;
QVERIFY2(containersCount >= 0,
QString("Containers count should be non-negative, but got %1")
.arg(containersCount)
.toUtf8().constData());
if (containersCount > 0) {
qDebug() << "Server has" << containersCount << "installed container(s)";
} else {
qDebug() << "Server has no installed containers";
}
for (auto it = containers.begin(); it != containers.end(); ++it) {
verifyClientConfig(it.value(), it.key());
}
QVERIFY2(scannedSelfHosted->containers.size() == containersCount,
QString("Scanned containers count should match. Expected: %1, Actual: %2")
.arg(containersCount)
.arg(scannedSelfHosted->containers.size())
.toUtf8().constData());
verifyAdminAccess(serverIndex);
qDebug() << "Test completed successfully. Server has admin access and all containers are initialized.";
}
void testRemoveAllContainers() {
ServerCredentials credentials = getCredentialsFromEnv();
if (credentials.hostName.isEmpty() || credentials.userName.isEmpty() || credentials.secretData.isEmpty()) {
QSKIP("Set SELF_HOSTED_SERVER_HOST_NAME, SELF_HOSTED_SERVER_USER_NAME, SELF_HOSTED_SERVER_PASSWORD");
}
QVERIFY2(credentials.isValid(), "Server credentials should be valid");
qDebug() << "Using server:" << credentials.hostName << "user:" << credentials.userName << "port:" << credentials.port;
verifySshConnection(credentials);
int awgPort = 55424;
TransportProto awgTransportProto = TransportProto::Udp;
bool wasAwgInstalled = false;
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
ErrorCode installServerError = m_coreController->m_installController->installServer(
credentials, DockerContainer::Awg, awgPort, awgTransportProto, wasAwgInstalled);
QVERIFY2(installServerError == ErrorCode::NoError,
QString("installServer for Awg should succeed. Error: %1")
.arg(static_cast<int>(installServerError))
.toUtf8().constData());
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
int serverIndex = m_coreController->m_serversRepository->serversCount() - 1;
qDebug() << "Server with Awg container added at index:" << serverIndex;
const auto adminBeforeRemoval = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(adminBeforeRemoval.has_value(), "Server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig *selfHostedBeforeRemoval = &(*adminBeforeRemoval);
QVERIFY2(!selfHostedBeforeRemoval->containers.isEmpty(), "Server should have containers before removal");
QVERIFY2(selfHostedBeforeRemoval->defaultContainer != DockerContainer::None, "Server should have default container before removal");
qDebug() << "Containers before removal:" << selfHostedBeforeRemoval->containers.size();
const QString removeServerId = m_coreController->m_serversRepository->serverIdAt(serverIndex);
ErrorCode removeError = m_coreController->m_installController->removeAllContainers(removeServerId);
QVERIFY2(removeError == ErrorCode::NoError,
QString("removeAllContainers should succeed. Error: %1")
.arg(static_cast<int>(removeError))
.toUtf8().constData());
qDebug() << "All containers removed successfully";
const auto adminAfterRemoval = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(serverIndex));
QVERIFY2(adminAfterRemoval.has_value(), "Server config should be SelfHostedAdminServerConfig");
const SelfHostedAdminServerConfig *selfHostedAfterRemoval = &(*adminAfterRemoval);
QVERIFY2(selfHostedAfterRemoval->containers.isEmpty(),
"Server should have no containers after removal");
QVERIFY2(selfHostedAfterRemoval->defaultContainer == DockerContainer::None,
"Default container should be None after removal");
qDebug() << "Containers after removal:" << selfHostedAfterRemoval->containers.size();
verifyAdminAccess(serverIndex);
qDebug() << "Test completed successfully. All containers removed and server is empty.";
}
};
QTEST_MAIN(TestSelfHostedServerSetup)
#include "testSelfHostedServerSetup.moc"
+195
View File
@@ -0,0 +1,195 @@
#include <QTest>
#include <QDebug>
#include <QFile>
#include <QSslSocket>
#include <QSslConfiguration>
#ifdef Q_OS_ANDROID
#include <QDir>
#include <QJniObject>
#include <QStandardPaths>
#include <dlfcn.h>
#include <fcntl.h>
#include <unistd.h>
#endif
// Verifies that the bundled OpenSSL is loaded on Android, not the system LibreSSL/Conscrypt.
// Uses only Qt's SSL abstraction so no direct libssl/libcrypto linkage is needed,
// which avoids bundling versioned .so.3 files that Android does not support.
class TestAndroidOpenSSL : public QObject
{
Q_OBJECT
private slots:
void testBundledLibraryLoaded()
{
const bool sslSupported = QSslSocket::supportsSsl();
qInfo() << "QSslSocket::supportsSsl() :" << sslSupported;
QVERIFY2(sslSupported,
"OpenSSL is not available – libssl.so / libcrypto.so "
"are not bundled in the APK or failed to load");
}
void testNotLibreSSL()
{
const QString runtimeVersion = QSslSocket::sslLibraryVersionString();
qInfo() << "SSL runtime version :" << runtimeVersion;
QVERIFY2(runtimeVersion.startsWith("OpenSSL "),
qPrintable(
QString("Unexpected SSL library on Android.\n"
" Got: %1\n"
" Expected prefix: 'OpenSSL '")
.arg(runtimeVersion)));
}
void testRuntimeVersionMatchesCompileTime()
{
const QString buildVersion = QSslSocket::sslLibraryBuildVersionString();
const QString runtimeVersion = QSslSocket::sslLibraryVersionString();
qInfo() << "SSL compile-time version :" << buildVersion;
qInfo() << "SSL runtime version :" << runtimeVersion;
qInfo() << "Runtime == Compile :" << (runtimeVersion == buildVersion ? "YES" : "NO");
QVERIFY2(runtimeVersion.startsWith("OpenSSL "),
"Runtime SSL library is not OpenSSL – wrong .so bundled in APK");
}
void testQtUsesOurOpenSSL()
{
const QString runtimeVersion = QSslSocket::sslLibraryVersionString();
const QString buildVersion = QSslSocket::sslLibraryBuildVersionString();
qInfo() << "Qt SSL runtime version :" << runtimeVersion;
qInfo() << "Qt SSL build version :" << buildVersion;
qInfo() << "Runtime == Build :" << (runtimeVersion == buildVersion ? "YES" : "NO");
// The bundled conan OpenSSL must be loaded (not the system Conscrypt)
QVERIFY2(runtimeVersion.startsWith("OpenSSL "),
qPrintable(
QString("Qt is not using our bundled OpenSSL.\n"
" Runtime : %1")
.arg(runtimeVersion)));
}
#ifdef Q_OS_ANDROID
// Verifies that libcrypto.so was loaded from the app's own APK bundle,
// not from /system/lib or /apex. Uses dladdr() on a well-known OpenSSL
// symbol to get the exact path of the mapped shared library.
void testLibcryptoLoadedFromAppBundle()
{
QVERIFY2(QSslSocket::supportsSsl(), "OpenSSL not loaded – cannot check bundle path");
// dlopen with RTLD_NOLOAD returns a handle to the already-loaded library
// without incrementing its reference count (no new load occurs).
void *handle = ::dlopen("libcrypto.so", RTLD_NOLOAD | RTLD_LAZY);
if (!handle) {
// Some builds use the versioned name
handle = ::dlopen("libcrypto_3.so", RTLD_NOLOAD | RTLD_LAZY);
}
QVERIFY2(handle != nullptr,
qPrintable(QString("libcrypto.so not found via dlopen(RTLD_NOLOAD): %1")
.arg(::dlerror())));
// Resolve a well-known symbol and ask dladdr for the library path
const void *sym = ::dlsym(handle, "OpenSSL_version");
::dlclose(handle);
QVERIFY2(sym != nullptr, "OpenSSL_version symbol not found in libcrypto");
Dl_info info{};
QVERIFY2(::dladdr(sym, &info) && info.dli_fname,
"dladdr failed to resolve libcrypto path");
const QString cryptoPath = QString::fromLocal8Bit(info.dli_fname);
qInfo() << "libcrypto.so loaded from:" << cryptoPath;
const bool fromSystem = cryptoPath.startsWith(QLatin1String("/system/"))
|| cryptoPath.startsWith(QLatin1String("/apex/"));
QVERIFY2(!fromSystem,
qPrintable(QString("System libcrypto.so loaded instead of bundled!\n"
" Path: %1")
.arg(cryptoPath)));
const bool fromApp = cryptoPath.startsWith(QLatin1String("/data/app/"))
|| cryptoPath.contains(
QLatin1String("org.qtproject.example.test_android_openssl"));
QVERIFY2(fromApp,
qPrintable(QString("libcrypto.so not from app bundle.\n"
" Path : %1\n"
" Expect: /data/app/...")
.arg(cryptoPath)));
}
#endif
};
#ifdef Q_OS_ANDROID
// On Android, androidtestrunner reads test output from files/stdout.txt.
// It starts `tail -f files/stdout.txt` immediately after launching the test,
// and KILLS the process if the file doesn't exist yet (exit=255).
//
// Fix: pre-create files/stdout.txt via JNI before QCoreApplication initializes
// (JNI is already set up by the Qt Activity host before main() is called).
// This gives androidtestrunner's tail something to attach to right away.
int main(int argc, char *argv[])
{
// Pre-create stdout.txt via JNI so androidtestrunner can tail it immediately.
QString filesDir;
{
QJniObject activity = QJniObject::callStaticObjectMethod(
"org/qtproject/qt/android/QtNative", "activity",
"()Landroid/app/Activity;");
if (activity.isValid()) {
QJniObject dir = activity.callObjectMethod(
"getFilesDir", "()Ljava/io/File;");
if (dir.isValid())
filesDir = dir.callObjectMethod<jstring>("getAbsolutePath").toString();
}
}
if (!filesDir.isEmpty()) {
const QByteArray path = (filesDir + "/stdout.txt").toLocal8Bit();
int fd = ::open(path.constData(), O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd >= 0) ::close(fd);
}
QCoreApplication app(argc, argv);
app.setAttribute(Qt::AA_Use96Dpi, true);
if (filesDir.isEmpty())
filesDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir().mkpath(filesDir);
const QString stdoutPath = filesDir + "/stdout.txt";
// Build a clean argument list from scratch with the absolute output path.
// Do NOT parse QCoreApplication::arguments() — on Android these include
// Intent extras that may already contain a relative "-o stdout.txt,txt",
// and attempting to patch them causes Qt to misinterpret the path as a
// test function name.
QStringList args;
args << QCoreApplication::applicationFilePath()
<< QStringLiteral("-o") << (stdoutPath + QStringLiteral(",txt"))
<< QStringLiteral("-v2");
TestAndroidOpenSSL tc;
const int result = QTest::qExec(&tc, args);
// Copy to Documents for easy manual inspection via the Files app.
const QString docsDir =
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
if (!docsDir.isEmpty()) {
QDir().mkpath(docsDir);
const QString dst = docsDir + "/test_android_openssl_results.txt";
QFile::remove(dst);
QFile::copy(stdoutPath, dst);
}
return result;
}
#else
QTEST_GUILESS_MAIN(TestAndroidOpenSSL)
#endif
#include "testAndroidOpenSSL.moc"
+152
View File
@@ -0,0 +1,152 @@
#include <QTest>
#include <QDebug>
#include <QSignalSpy>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include "utils/testCoreController.h"
#include "utils/testUtils.h"
#include "core/controllers/settingsController.h"
#include "core/repositories/secureServersRepository.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestAndroidRestoreBackup : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
void importServer(const QString &key)
{
auto result = m_coreController->m_importCoreController->extractConfigFromData(key);
QVERIFY2(result.errorCode == ErrorCode::NoError, "Server import must succeed");
m_coreController->m_importCoreController->importConfig(result.config);
}
private slots:
void initTestCase()
{
const QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase()
{
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init()
{
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
}
// -----------------------------------------------------------------------
// Android safety: killSwitch and autoConnect must be DISABLED after restore
// even if the backup was created with them enabled.
// (SettingsController::restoreAppConfigFromData unconditionally resets
// these flags on Android / iOS – see Q_OS_ANDROID block in the impl.)
// -----------------------------------------------------------------------
void testRestoreDisablesDangerousSettingsOnAndroid()
{
const QString key = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
if (!isEnvValueConfigured(key)) {
QSKIP("THIRD_PARTY_AWG_VPN_KEY not set – skipping");
}
importServer(key);
// Enable dangerous settings before creating the backup
m_coreController->m_appSettingsRepository->setKillSwitchEnabled(true);
m_coreController->m_appSettingsRepository->setAutoConnect(true);
QVERIFY(m_coreController->m_appSettingsRepository->isKillSwitchEnabled());
QVERIFY(m_coreController->m_appSettingsRepository->isAutoConnect());
const QByteArray backup = m_coreController->m_settingsController->backupAppConfig();
QVERIFY(!backup.isEmpty());
const QJsonObject backupJson = QJsonDocument::fromJson(backup).object();
qInfo() << "Backup killSwitchEnabled :" << backupJson.value("Conf/killSwitchEnabled").toBool();
qInfo() << "Backup autoConnect :" << backupJson.value("Conf/autoConnect").toBool();
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
const ErrorCode err = m_coreController->m_settingsController->restoreAppConfigFromData(backup);
QVERIFY2(err == ErrorCode::NoError, "Restore must succeed");
const bool killSwitch = m_coreController->m_appSettingsRepository->isKillSwitchEnabled();
const bool autoConnect = m_coreController->m_appSettingsRepository->isAutoConnect();
qInfo() << "killSwitchEnabled after restore:" << killSwitch;
qInfo() << "autoConnect after restore :" << autoConnect;
QVERIFY2(!killSwitch,
"killSwitch must be DISABLED after restore on Android (safety reset)");
QVERIFY2(!autoConnect,
"autoConnect must be DISABLED after restore on Android (safety reset)");
}
// -----------------------------------------------------------------------
// Cross-platform backup: restoring a backup from another platform must
// succeed and must clear the app split-tunneling apps list on Android.
// -----------------------------------------------------------------------
void testRestoreFromOtherPlatformClearsAppSplitTunnelingList()
{
const QString key = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
if (!isEnvValueConfigured(key)) {
QSKIP("THIRD_PARTY_AWG_VPN_KEY not set – skipping");
}
importServer(key);
// Build a synthetic backup that claims to come from Windows
QByteArray baseBackup = m_coreController->m_settingsController->backupAppConfig();
QJsonObject obj = QJsonDocument::fromJson(baseBackup).object();
obj["AppPlatform"] = "Windows";
const QByteArray crossPlatformBackup = QJsonDocument(obj).toJson();
QSignalSpy clearAppsListSpy(m_coreController->m_settingsController,
&SettingsController::appSplitTunnelingClearAppsList);
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
const ErrorCode err =
m_coreController->m_settingsController->restoreAppConfigFromData(crossPlatformBackup);
QVERIFY2(err == ErrorCode::NoError, "Cross-platform restore must succeed");
qInfo() << "appSplitTunnelingClearAppsList emitted:" << clearAppsListSpy.count() << "time(s)";
QVERIFY2(clearAppsListSpy.count() == 1,
"appSplitTunnelingClearAppsList must be emitted when restoring from different platform");
}
// -----------------------------------------------------------------------
// Invalid data must return an error, not crash
// -----------------------------------------------------------------------
void testRestoreInvalidDataReturnsError()
{
const QByteArray garbage = QByteArray("not a valid backup {{{{");
const ErrorCode err = m_coreController->m_settingsController->restoreAppConfigFromData(garbage);
qInfo() << "ErrorCode for invalid backup:" << static_cast<int>(err);
QVERIFY2(err != ErrorCode::NoError,
"Restoring invalid data must return an error code");
}
};
QTEST_MAIN(TestAndroidRestoreBackup)
#include "testAndroidRestoreBackup.moc"
+110
View File
@@ -0,0 +1,110 @@
#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");
}
QVERIFY2(QFileInfo(backup).size() > 0,
"BACKUP_PATH file is empty - is the BACKUP_FILE_BASE64 secret configured?");
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"
+206
View File
@@ -0,0 +1,206 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSignalSpy>
#include <QTest>
#include <QUuid>
#include "utils/testCoreController.h"
#include "core/models/api/apiV2ServerConfig.h"
#include "core/models/selfhosted/nativeServerConfig.h"
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "core/models/selfhosted/selfHostedUserServerConfig.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/serverConfigUtils.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
namespace
{
QJsonObject adminConfigJson(const QString &hostName, int formatVersion = -1)
{
QJsonObject json;
json[configKey::hostName] = hostName;
json[configKey::userName] = QStringLiteral("root");
json[configKey::password] = QStringLiteral("secret");
json[configKey::port] = 22;
json[configKey::description] = QStringLiteral("Server ") + hostName;
if (formatVersion >= 0) {
json[configKey::formatVersion] = formatVersion;
}
return json;
}
QByteArray serversListBytes(const QList<QJsonObject> &configs)
{
QJsonArray array;
for (const QJsonObject &config : configs) {
array.append(config);
}
return QJsonDocument(array).toJson();
}
} // namespace
class TestConfigFormatVersion : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController = nullptr;
SecureQSettings *m_settings = nullptr;
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();
}
void testVersionCheckHelpers()
{
QCOMPARE(serverConfigUtils::configFormatVersion(QJsonObject()), 0);
QVERIFY(serverConfigUtils::isConfigFormatVersionSupported(QJsonObject()));
QVERIFY(serverConfigUtils::isConfigFormatVersionSupported(
adminConfigJson("1.1.1.1", serverConfigUtils::currentConfigFormatVersion)));
QVERIFY(!serverConfigUtils::isConfigFormatVersionSupported(
adminConfigJson("1.1.1.1", serverConfigUtils::currentConfigFormatVersion + 1)));
}
void testToJsonWritesCurrentFormatVersion()
{
SelfHostedAdminServerConfig admin;
admin.hostName = "1.1.1.1";
QCOMPARE(admin.toJson().value(configKey::formatVersion).toInt(-1), serverConfigUtils::currentConfigFormatVersion);
SelfHostedUserServerConfig user;
user.hostName = "1.1.1.1";
QCOMPARE(user.toJson().value(configKey::formatVersion).toInt(-1), serverConfigUtils::currentConfigFormatVersion);
NativeServerConfig native;
native.hostName = "1.1.1.1";
QCOMPARE(native.toJson().value(configKey::formatVersion).toInt(-1), serverConfigUtils::currentConfigFormatVersion);
ApiV2ServerConfig api;
api.hostName = "1.1.1.1";
QCOMPARE(api.toJson().value(configKey::formatVersion).toInt(-1), serverConfigUtils::currentConfigFormatVersion);
}
void testImportAcceptsMissingAndCurrentVersion()
{
auto *importController = m_coreController->m_importCoreController;
QSignalSpy importFinishedSpy(importController, &ImportController::importFinished);
const QString withoutVersion = QJsonDocument(adminConfigJson("1.1.1.1")).toJson();
auto result = importController->extractConfigFromData(withoutVersion);
QCOMPARE(result.errorCode, ErrorCode::NoError);
importController->importConfig(result.config);
const QString currentVersion =
QJsonDocument(adminConfigJson("2.2.2.2", serverConfigUtils::currentConfigFormatVersion)).toJson();
result = importController->extractConfigFromData(currentVersion);
QCOMPARE(result.errorCode, ErrorCode::NoError);
importController->importConfig(result.config);
QCOMPARE(importFinishedSpy.count(), 2);
QCOMPARE(m_coreController->m_serversRepository->serversCount(), 2);
const auto stored = m_coreController->m_serversRepository->selfHostedAdminConfig(
m_coreController->m_serversRepository->serverIdAt(0));
QVERIFY(stored.has_value());
QCOMPARE(stored->toJson().value(configKey::formatVersion).toInt(-1), serverConfigUtils::currentConfigFormatVersion);
}
void testImportRejectsNewerVersion()
{
auto *importController = m_coreController->m_importCoreController;
QSignalSpy importFinishedSpy(importController, &ImportController::importFinished);
QSignalSpy importErrorSpy(importController, &ImportController::importErrorOccurred);
const QJsonObject newer = adminConfigJson("3.3.3.3", serverConfigUtils::currentConfigFormatVersion + 1);
const QByteArray newerBytes = QJsonDocument(newer).toJson();
auto result = importController->extractConfigFromData(QString::fromUtf8(newerBytes));
QCOMPARE(result.errorCode, ErrorCode::ConfigFormatVersionNotSupportedError);
QVERIFY(result.config.isEmpty());
const QString vpnUrl = "vpn://"
+ QString::fromUtf8(qCompress(newerBytes, 8).toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
result = importController->extractConfigFromData(vpnUrl);
QCOMPARE(result.errorCode, ErrorCode::ConfigFormatVersionNotSupportedError);
QVERIFY(result.config.isEmpty());
result = importController->extractConfigFromQr(newerBytes);
QCOMPARE(result.errorCode, ErrorCode::ConfigFormatVersionNotSupportedError);
QVERIFY(result.config.isEmpty());
importController->importConfig(newer);
QCOMPARE(importErrorSpy.count(), 1);
QCOMPARE(importErrorSpy.first().at(0).value<ErrorCode>(), ErrorCode::ConfigFormatVersionNotSupportedError);
QCOMPARE(importFinishedSpy.count(), 0);
QCOMPARE(m_coreController->m_serversRepository->serversCount(), 0);
}
void testRepositorySkipsNewerVersionOnLoad()
{
m_settings->setValue("Servers/serversList",
serversListBytes({ adminConfigJson("1.1.1.1"),
adminConfigJson("2.2.2.2", serverConfigUtils::currentConfigFormatVersion + 1),
adminConfigJson("3.3.3.3", serverConfigUtils::currentConfigFormatVersion) }));
m_coreController->m_serversRepository->invalidateCache();
QCOMPARE(m_coreController->m_serversRepository->serversCount(), 2);
QCOMPARE(m_coreController->m_serversRepository->unsupportedFormatConfigsCount(), 1);
const auto rejected = m_coreController->m_serversRepository->addServer(
QString(), adminConfigJson("4.4.4.4", serverConfigUtils::currentConfigFormatVersion + 1),
serverConfigUtils::ConfigType::SelfHostedAdmin);
QVERIFY(!rejected.isEmpty());
QCOMPARE(m_coreController->m_serversRepository->serversCount(), 2);
}
void testRestoreBackupReportsSkippedConfigs()
{
QJsonObject backup;
backup["Servers/serversList"] = QString::fromUtf8(serversListBytes(
{ adminConfigJson("1.1.1.1"),
adminConfigJson("2.2.2.2", serverConfigUtils::currentConfigFormatVersion + 1) }));
QSignalSpy errorSpy(m_coreController->m_settingsUiController, &SettingsUiController::errorOccurred);
QSignalSpy restoredSpy(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished);
m_coreController->m_settingsUiController->restoreAppConfigFromData(QJsonDocument(backup).toJson());
QCOMPARE(restoredSpy.count(), 1);
QCOMPARE(errorSpy.count(), 1);
QCOMPARE(errorSpy.first().at(0).value<ErrorCode>(), ErrorCode::RestoreBackupUnsupportedConfigsSkipped);
QCOMPARE(m_coreController->m_serversRepository->serversCount(), 1);
errorSpy.clear();
restoredSpy.clear();
QJsonObject cleanBackup;
cleanBackup["Servers/serversList"] = QString::fromUtf8(serversListBytes({ adminConfigJson("5.5.5.5") }));
m_coreController->m_settingsUiController->restoreAppConfigFromData(QJsonDocument(cleanBackup).toJson());
QCOMPARE(restoredSpy.count(), 1);
QCOMPARE(errorSpy.count(), 0);
}
};
QTEST_MAIN(TestConfigFormatVersion)
#include "testConfigFormatVersion.moc"
+141
View File
@@ -0,0 +1,141 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
#include "core/utils/utilities.h"
#include "version.h"
using namespace amnezia;
using namespace amnezia::test;
class TestConnection : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), "");
}
}
void testConnect() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
QString wgKey = getEnvValue("THIRD_PARTY_WIRE_GUARD_VPN_KEY");
QString ovpnKey = getEnvValue("THIRD_PARTY_OPEN_VPN_KEY");
QString ikevKey = getEnvValue("THIRD_PARTY_IKEV_VPN_KEY");
logEnvValueState("THIRD_PARTY_AWG_VPN_KEY");
logEnvValueState("THIRD_PARTY_XRAY_VPN_KEY");
logEnvValueState("THIRD_PARTY_WIRE_GUARD_VPN_KEY");
logEnvValueState("THIRD_PARTY_OPEN_VPN_KEY");
logEnvValueState("THIRD_PARTY_IKEV_VPN_KEY");
// toggleConnection() silently refuses to emit prepareConfig when the
// AmneziaVPN service process is not running (isConnectionSupported returns
// AmneziaServiceNotRunning), so this test needs an installed client.
if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true)) {
QSKIP("AmneziaVPN service is not running - connection flow cannot be tested");
}
QSignalSpy prepareConfigSpy(m_coreController->m_connectionUiController, &ConnectionUiController::prepareConfig);
auto importResult0 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
if (importResult0.errorCode == ErrorCode::NoError) {
m_coreController->m_importCoreController->importConfig(importResult0.config);
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(prepareConfigSpy.count() == 1, "prepareConfig signal should be emitted once");
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(m_coreController->m_connectionUiController->isConnected() == false, "Connection should NOT be active");
} else {
qWarning() << "Error on AWG key import";
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
if (importResult1.errorCode == ErrorCode::NoError) {
m_coreController->m_importCoreController->importConfig(importResult1.config);
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(prepareConfigSpy.count() == 3, "prepareConfig signal should be emitted three times");
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(m_coreController->m_connectionUiController->isConnected() == false, "Connection should NOT be active");
} else {
qWarning() << "Error on XRay key import";
}
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
if (importResult2.errorCode == ErrorCode::NoError) {
m_coreController->m_importCoreController->importConfig(importResult2.config);
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(prepareConfigSpy.count() == 5, "prepareConfig signal should be emitted five times");
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(m_coreController->m_connectionUiController->isConnected() == false, "Connection should NOT be active");
} else {
qWarning() << "Error on WG key import";
}
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(ovpnKey);
if (importResult3.errorCode == ErrorCode::NoError) {
m_coreController->m_importCoreController->importConfig(importResult3.config);
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(prepareConfigSpy.count() == 7, "prepareConfig signal should be emitted seven times");
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(m_coreController->m_connectionUiController->isConnected() == false, "Connection should NOT be active");
} else {
qWarning() << "Error on OpenVPN key import";
}
auto importResult4 = m_coreController->m_importCoreController->extractConfigFromData(ikevKey);
if (importResult4.errorCode == ErrorCode::NoError) {
m_coreController->m_importCoreController->importConfig(importResult4.config);
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(prepareConfigSpy.count() == 9, "prepareConfig signal should be emitted nine times");
m_coreController->m_connectionUiController->toggleConnection();
QVERIFY2(m_coreController->m_connectionUiController->isConnected() == false, "Connection should NOT be active");
}
else {
qWarning() << "Error on IKEV key import";
}
}
};
QTEST_MAIN(TestConnection)
#include "testConnection.moc"
+136
View File
@@ -0,0 +1,136 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "ui/models/serversModel.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestDefaultServerChange : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testSetDefaultServerIndex() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
QString wgKey = getEnvValue("THIRD_PARTY_WIRE_GUARD_VPN_KEY");
if (!isEnvValueConfigured(awgKey) || !isEnvValueConfigured(xrayKey) || !isEnvValueConfigured(wgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY, THIRD_PARTY_XRAY_VPN_KEY, THIRD_PARTY_WIRE_GUARD_VPN_KEY");
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
m_coreController->m_importCoreController->importConfig(importResult3.config);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted");
QVERIFY2(defaultServerChangedSpy.at(0).at(0).toString() == m_coreController->m_serversController->getServerId(0),
"defaultServerChanged should emit new default server id");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0");
if (m_coreController->m_serversModel) {
int modelDefaultIndex = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool() ? 0 : -1;
QVERIFY2(modelDefaultIndex == 0, "Model should reflect default server");
}
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(2));
QVERIFY2(defaultServerChangedSpy.count() == 2, "defaultServerChanged signal should be emitted again");
QVERIFY2(defaultServerChangedSpy.at(1).at(0).toString() == m_coreController->m_serversController->getServerId(2),
"defaultServerChanged should emit new default server id");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default server index should be 2");
}
void testDefaultServerChangeOnRemoveEdgeCases() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
QString wgKey = getEnvValue("THIRD_PARTY_WIRE_GUARD_VPN_KEY");
if (!isEnvValueConfigured(awgKey) || !isEnvValueConfigured(xrayKey) || !isEnvValueConfigured(wgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY, THIRD_PARTY_XRAY_VPN_KEY, THIRD_PARTY_WIRE_GUARD_VPN_KEY");
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
m_coreController->m_importCoreController->importConfig(importResult3.config);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 3, "Should have 3 servers");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 2, "Should have 2 servers");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1 (was 2, removed 0)");
const auto description1 = serverDescriptionAt(m_coreController->m_serversRepository, 0);
const auto description2 = serverDescriptionAt(m_coreController->m_serversRepository, 1);
QVERIFY2(description1.has_value() && description2.has_value(), "Server configs should exist");
QVERIFY2(*description1 == "Xray Server", "First remaining server should be Xray");
QVERIFY2(*description2 == "WireGuard Server", "Second remaining server should be WireGuard");
defaultServerChangedSpy.clear();
serverRemovedSpy.clear();
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have 1 server");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default should be index 0 (was 1, removed 0)");
const auto lastDescription = serverDescriptionAt(m_coreController->m_serversRepository, 0);
QVERIFY2(lastDescription.has_value(), "Server config should exist");
QVERIFY2(*lastDescription == "WireGuard Server", "Last server should be WireGuard");
}
};
QTEST_MAIN(TestDefaultServerChange)
#include "testDefaultServerChange.moc"
+216
View File
@@ -0,0 +1,216 @@
#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() {
const QString dnsListPath = QFINDTESTDATA("data/dns_list.json");
const QString ipListPath = QFINDTESTDATA("data/ip_list.json");
QVERIFY2(!dnsListPath.isEmpty(), "data/dns_list.json not found");
QVERIFY2(!ipListPath.isEmpty(), "data/ip_list.json not found");
QSignalSpy dnsErrorOccurredSpy(m_coreController->m_allowedDnsUiController, &AllowedDnsUiController::errorOccurred);
QSignalSpy dnsFinishedSpy(m_coreController->m_allowedDnsUiController, &AllowedDnsUiController::finished);
QSignalSpy ipErrorOccurredSpy(m_coreController->m_ipSplitTunnelingUiController, &IpSplitTunnelingUiController::errorOccurred);
QSignalSpy ipFinishedSpy(m_coreController->m_ipSplitTunnelingUiController, &IpSplitTunnelingUiController::finished);
m_coreController->m_allowedDnsUiController->importDns(dnsListPath, true);
if (dnsErrorOccurredSpy.count() > 0) {
qWarning() << "(dns) errorOccurred:" << dnsErrorOccurredSpy.at(0).at(0).toString();
}
QVERIFY2(dnsErrorOccurredSpy.count() == 0, "(dns) errorOccurred signal should NOT be emitted");
QVERIFY2(dnsFinishedSpy.count() == 1, "(dns) finished signal should be emitted");
m_coreController->m_ipSplitTunnelingUiController->importSites(ipListPath, true);
if (ipErrorOccurredSpy.count() > 0) {
qWarning() << "(ip) errorOccurred:" << ipErrorOccurredSpy.at(0).at(0).toString();
}
QVERIFY2(ipErrorOccurredSpy.count() == 0, "(ip) errorOccurred signal should NOT be emitted");
QVERIFY2(ipFinishedSpy.count() == 1, "(ip) finished signal should be emitted");
}
};
QTEST_MAIN(TestMultipleImports)
#include "testMultipleImports.moc"
+93
View File
@@ -0,0 +1,93 @@
#include <QTest>
#include <QDebug>
#include <QSslSocket>
#include <openssl/crypto.h>
#include <openssl/opensslv.h>
class TestOpenSSLLibrary : public QObject
{
Q_OBJECT
private slots:
void testRuntimeVersionMatchesCompileTime()
{
const QString compileVersion = QString::fromUtf8(OPENSSL_VERSION_TEXT);
const QString runtimeVersion = QString::fromUtf8(OpenSSL_version(OPENSSL_VERSION));
qInfo() << "OpenSSL compile-time version :" << compileVersion;
qInfo() << "OpenSSL runtime version :" << runtimeVersion;
// If the system library (e.g. macOS LibreSSL) was loaded instead of ours,
// the runtime string will differ from the compile-time macro.
QVERIFY2(runtimeVersion == compileVersion,
qPrintable(
QString("OpenSSL version mismatch – system library loaded instead of bundled one.\n"
" Compile-time : %1\n"
" Runtime : %2")
.arg(compileVersion, runtimeVersion)));
}
void testNotSystemLibreSSL()
{
const QString runtimeVersion = QString::fromUtf8(OpenSSL_version(OPENSSL_VERSION));
qInfo() << "OpenSSL runtime version:" << runtimeVersion;
QVERIFY2(runtimeVersion.startsWith("OpenSSL "),
qPrintable(
QString("System library loaded instead of bundled OpenSSL.\n"
" Got: %1\n"
" Expected prefix: 'OpenSSL '")
.arg(runtimeVersion)));
}
void testQtUsesOurOpenSSL()
{
const QString qtRuntimeVersion = QSslSocket::sslLibraryVersionString();
const QString qtBuildVersion = QSslSocket::sslLibraryBuildVersionString();
const QString ourRuntime = QString::fromUtf8(OpenSSL_version(OPENSSL_VERSION));
qInfo() << "Qt SSL runtime version :" << qtRuntimeVersion;
qInfo() << "Qt SSL build version :" << qtBuildVersion;
qInfo() << "App OpenSSL runtime :" << ourRuntime;
qInfo() << "Qt runtime == App runtime:" << (qtRuntimeVersion == ourRuntime ? "YES" : "NO");
QVERIFY2(qtRuntimeVersion.startsWith("OpenSSL "),
qPrintable("Qt is not using OpenSSL at runtime, got: " + qtRuntimeVersion));
#ifndef Q_OS_WIN
QVERIFY2(qtRuntimeVersion == ourRuntime,
qPrintable(
QString("Qt SSL library differs from the OpenSSL linked by the app.\n"
" Qt runtime : %1\n"
" App runtime : %2")
.arg(qtRuntimeVersion, ourRuntime)));
#endif
}
#ifdef Q_OS_WIN
void testWindowsNoDllHijack()
{
const QString runtimeVersion = QString::fromUtf8(OpenSSL_version(OPENSSL_VERSION));
const QString compileVersion = QString::fromUtf8(OPENSSL_VERSION_TEXT);
const QString opensslDir = QString::fromUtf8(OpenSSL_version(OPENSSL_DIR));
qInfo() << "Windows OpenSSL runtime version :" << runtimeVersion;
qInfo() << "Windows OpenSSL compile version :" << compileVersion;
qInfo() << "Windows OpenSSL configured dir :" << opensslDir;
qInfo() << "Runtime == Compile :" << (runtimeVersion == compileVersion ? "YES" : "NO");
QVERIFY2(runtimeVersion == compileVersion,
qPrintable(
QString("Wrong OpenSSL DLL loaded on Windows (possible PATH hijack).\n"
" Expected (compile-time) : %1\n"
" Got (runtime) : %2\n"
" OpenSSL configured dir : %3")
.arg(compileVersion, runtimeVersion, opensslDir)));
}
#endif // Q_OS_WIN
};
QTEST_MAIN(TestOpenSSLLibrary)
#include "testOpenSSLLibrary.moc"
+121
View File
@@ -0,0 +1,121 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/repositories/secureServersRepository.h"
#include "core/models/serverDescription.h"
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
#include "core/utils/serverConfigUtils.h"
using namespace amnezia;
using namespace amnezia::test;
class TestServerEdgeCases : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testInvalidIndexOperations() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
if (!isEnvValueConfigured(awgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY");
}
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have 1 server");
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(-1));
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(10));
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(100));
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for invalid index");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server count should remain 1");
const QString validServerId = m_coreController->m_serversController->getServerId(0);
const serverConfigUtils::ConfigType editKind =
m_coreController->m_serversRepository->serverKind(validServerId);
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(-1),
QJsonObject(), editKind);
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for invalid index");
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(10),
QJsonObject(), editKind);
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for invalid index");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(-1));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for invalid index");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(10));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for invalid index");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should remain 0");
}
void testEmptyRepositoryOperations() {
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Should start with 0 servers");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(serverRemovedSpy.count() == 0, "serverRemoved should NOT be emitted for empty repository");
m_coreController->m_serversRepository->editServer(m_coreController->m_serversController->getServerId(0),
SelfHostedAdminServerConfig {}.toJson(),
serverConfigUtils::ConfigType::SelfHostedAdmin);
QVERIFY2(serverEditedSpy.count() == 0, "serverEdited should NOT be emitted for empty repository");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted for empty repository");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0 for empty repository");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 0, "Server count should remain 0");
}
};
QTEST_MAIN(TestServerEdgeCases)
#include "testServerEdgeCases.moc"
+113
View File
@@ -0,0 +1,113 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "ui/models/serversModel.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestServerEdit : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testServerEditTriggersHandlers() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
if (!isEnvValueConfigured(awgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY");
}
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(importFinishedSpy.count() == 1, "Import should succeed");
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
QVERIFY(m_coreController->m_serversController->renameServer(
m_coreController->m_serversController->getServerId(0), QStringLiteral("Edited AWG Server")));
QVERIFY2(serverEditedSpy.count() == 1, "serverEdited signal should be emitted");
QVERIFY2(serverEditedSpy.at(0).at(0).toString() == m_coreController->m_serversRepository->serverIdAt(0),
"serverEdited should emit edited server id");
const auto editedDescription = serverDescription(m_coreController->m_serversRepository,
m_coreController->m_serversRepository->serverIdAt(0));
QVERIFY2(editedDescription.has_value(), "Server config should exist");
QVERIFY2(*editedDescription == "Edited AWG Server", "Server description should be updated");
if (m_coreController->m_serversModel) {
QString modelDesc = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc == "Edited AWG Server", "Server description in model should be updated");
}
}
void testServerEditPreservesDefault() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
if (!isEnvValueConfigured(awgKey) || !isEnvValueConfigured(xrayKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY and THIRD_PARTY_XRAY_VPN_KEY");
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server should be index 1");
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
QVERIFY(m_coreController->m_serversController->renameServer(
m_coreController->m_serversController->getServerId(1), QStringLiteral("Edited Default Server")));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted when editing default server");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server index should remain 1");
QVERIFY(m_coreController->m_serversController->renameServer(
m_coreController->m_serversController->getServerId(0), QStringLiteral("Edited Non-Default Server")));
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged should NOT be emitted when editing non-default server");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default server index should remain 1");
}
};
QTEST_MAIN(TestServerEdit)
#include "testServerEdit.moc"
+100
View File
@@ -0,0 +1,100 @@
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "ui/models/serversModel.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestServerRename : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
void importSingleServer()
{
const QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
const auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
QVERIFY2(importResult.errorCode == ErrorCode::NoError, "Import setup should succeed");
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Should have one server");
}
private slots:
void initTestCase()
{
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase()
{
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init()
{
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testRenameServerUpdatesRepositoryAndModel()
{
if (!isEnvValueConfigured(getEnvValue("THIRD_PARTY_AWG_VPN_KEY"))) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY");
}
importSingleServer();
const QString serverId = m_coreController->m_serversController->getServerId(0);
QSignalSpy serverEditedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverEdited);
QVERIFY(m_coreController->m_serversController->renameServer(serverId, QStringLiteral("Renamed Server")));
QVERIFY2(serverEditedSpy.count() == 1, "serverEdited should be emitted");
const auto renamedDescription = serverDescription(m_coreController->m_serversRepository, serverId);
QVERIFY2(renamedDescription.has_value(), "Server config should exist");
QVERIFY2(*renamedDescription == "Renamed Server", "Repository should store renamed description");
if (m_coreController->m_serversModel) {
const QString modelName = m_coreController->m_serversModel
->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole)
.toString();
QVERIFY2(modelName == "Renamed Server", "Model should reflect renamed server");
}
QVERIFY(m_coreController->m_serversController->renameServer(serverId, QStringLiteral("Renamed Again")));
QVERIFY2(serverEditedSpy.count() == 2, "serverEdited should be emitted again");
const auto renamedAgainDescription = serverDescription(m_coreController->m_serversRepository, serverId);
QVERIFY2(renamedAgainDescription.has_value(), "Server config should exist");
QVERIFY2(*renamedAgainDescription == "Renamed Again", "Repository should store second rename");
if (m_coreController->m_serversModel) {
const QString modelName = m_coreController->m_serversModel
->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole)
.toString();
QVERIFY2(modelName == "Renamed Again", "Model should reflect second rename");
}
}
};
QTEST_MAIN(TestServerRename)
#include "testServerRename.moc"
+120
View File
@@ -0,0 +1,120 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "ui/models/serversModel.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestServersModelSync : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testServersModelSyncOnOperations() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
if (!m_coreController->m_serversModel) {
QSKIP("ServersModel not available");
}
if (!isEnvValueConfigured(awgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY");
}
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Initial model row count should be 0");
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "Model should have 1 row after import");
QString modelDesc1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc1 == "AWG Server", "Model should have correct server name");
QVERIFY(m_coreController->m_serversController->renameServer(
m_coreController->m_serversController->getServerId(0), QStringLiteral("Edited AWG Server")));
QString modelDesc2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::NameRole).toString();
QVERIFY2(modelDesc2 == "Edited AWG Server", "Model should be updated after edit");
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(0));
QVERIFY2(m_coreController->m_serversModel->rowCount() == 0, "Model should have 0 rows after removal");
}
void testServersModelDefaultIndexSync() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
QString wgKey = getEnvValue("THIRD_PARTY_WIRE_GUARD_VPN_KEY");
if (!m_coreController->m_serversModel) {
QSKIP("ServersModel not available");
}
if (!isEnvValueConfigured(awgKey) || !isEnvValueConfigured(xrayKey) || !isEnvValueConfigured(wgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY, THIRD_PARTY_XRAY_VPN_KEY, THIRD_PARTY_WIRE_GUARD_VPN_KEY");
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
auto importResult3 = m_coreController->m_importCoreController->extractConfigFromData(wgKey);
m_coreController->m_importCoreController->importConfig(importResult3.config);
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 2, "Default should be index 2");
QVERIFY2(m_coreController->m_serversModel->rowCount() == 3, "Model should have 3 rows");
bool isDefault0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool();
bool isDefault1 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(1, 0), ServersModel::IsDefaultRole).toBool();
bool isDefault2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::IsDefaultRole).toBool();
QVERIFY2(!isDefault0, "Server 0 should not be default");
QVERIFY2(!isDefault1, "Server 1 should not be default");
QVERIFY2(isDefault2, "Server 2 should be default");
m_coreController->m_serversController->setDefaultServer(m_coreController->m_serversController->getServerId(0));
isDefault0 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(0, 0), ServersModel::IsDefaultRole).toBool();
isDefault2 = m_coreController->m_serversModel->data(m_coreController->m_serversModel->index(2, 0), ServersModel::IsDefaultRole).toBool();
QVERIFY2(isDefault0, "Server 0 should be default after change");
QVERIFY2(!isDefault2, "Server 2 should not be default after change");
}
};
QTEST_MAIN(TestServersModelSync)
#include "testServersModelSync.moc"
+267
View File
@@ -0,0 +1,267 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QLocale>
#include <QTest>
#include "utils/testCoreController.h"
#include "ui/controllers/settingsUiController.h"
#include "ui/controllers/languageUiController.h"
#include "ui/models/allowedDnsModel.h"
#include "ui/models/ipSplitTunnelingModel.h"
#include "ui/models/appSplitTunnelingModel.h"
#include "ui/models/languageModel.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
class TestSettingsSignals : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
}
void testDnsSettingsSignals() {
QSignalSpy primaryDnsChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::primaryDnsChanged);
QSignalSpy secondaryDnsChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::secondaryDnsChanged);
QSignalSpy allowedDnsServersChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::allowedDnsServersChanged);
QString primaryDns = "8.8.8.8";
QString secondaryDns = "8.8.4.4";
m_coreController->m_settingsUiController->setPrimaryDns(primaryDns);
QVERIFY2(primaryDnsChangedSpy.count() == 1, "primaryDnsChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->getPrimaryDns() == primaryDns, "Primary DNS should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->getPrimaryDns() == primaryDns, "Primary DNS should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->primaryDns() == primaryDns, "Primary DNS should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->setSecondaryDns(secondaryDns);
QVERIFY2(secondaryDnsChangedSpy.count() == 1, "secondaryDnsChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->getSecondaryDns() == secondaryDns, "Secondary DNS should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->getSecondaryDns() == secondaryDns, "Secondary DNS should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->secondaryDns() == secondaryDns, "Secondary DNS should be available in SecureAppSettingsRepository");
QStringList dnsList = {"1.1.1.1", "1.0.0.1"};
m_coreController->m_allowedDnsController->addDnsList(dnsList, true);
QVERIFY2(allowedDnsServersChangedSpy.count() == 1, "allowedDnsServersChanged signal should be emitted");
QVERIFY2(m_coreController->m_appSettingsRepository->getAllowedDnsServers() == dnsList, "Allowed DNS servers should be updated in SecureAppSettingsRepository");
QVERIFY2(m_coreController->m_allowedDnsController->getCurrentDnsServers() == dnsList, "Allowed DNS servers should be available in AllowedDnsController");
QVERIFY2(m_coreController->m_allowedDnsUiController != nullptr, "AllowedDnsUiController should exist");
QVERIFY2(m_coreController->m_allowedDnsModel != nullptr, "AllowedDnsModel should exist");
QStringList modelDnsList;
for (int i = 0; i < m_coreController->m_allowedDnsModel->rowCount(); ++i) {
modelDnsList.append(m_coreController->m_allowedDnsModel->data(m_coreController->m_allowedDnsModel->index(i, 0), AllowedDnsModel::IpRole).toString());
}
QVERIFY2(modelDnsList == dnsList, "Allowed DNS servers should be available in AllowedDnsModel");
}
void testAmneziaDnsToggleSignal() {
QSignalSpy amneziaDnsToggledSpy(m_coreController->m_settingsUiController, &SettingsUiController::amneziaDnsToggled);
QSignalSpy useAmneziaDnsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::useAmneziaDnsChanged);
bool initialValue = m_coreController->m_settingsController->isAmneziaDnsEnabled();
m_coreController->m_settingsUiController->toggleAmneziaDns(!initialValue);
QVERIFY2(amneziaDnsToggledSpy.count() == 1, "amneziaDnsToggled signal should be emitted");
QVERIFY2(amneziaDnsToggledSpy.at(0).at(0).toBool() == !initialValue, "amneziaDnsToggled should emit correct value");
QVERIFY2(useAmneziaDnsChangedSpy.count() == 1, "useAmneziaDnsChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->isAmneziaDnsEnabled() == !initialValue, "Amnezia DNS state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isAmneziaDnsEnabled() == !initialValue, "Amnezia DNS state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->useAmneziaDns() == !initialValue, "Amnezia DNS state should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->toggleAmneziaDns(initialValue);
QVERIFY2(amneziaDnsToggledSpy.count() == 2, "amneziaDnsToggled signal should be emitted again");
QVERIFY2(useAmneziaDnsChangedSpy.count() == 2, "useAmneziaDnsChanged signal should be emitted again");
QVERIFY2(m_coreController->m_settingsUiController->isAmneziaDnsEnabled() == initialValue, "Amnezia DNS state should be restored in SettingsUiController");
}
void testLoggingSignals() {
QSignalSpy loggingStateChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::loggingStateChanged);
QSignalSpy saveLogsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::saveLogsChanged);
bool initialLogging = m_coreController->m_settingsController->isLoggingEnabled();
m_coreController->m_settingsUiController->toggleLogging(!initialLogging);
QVERIFY2(loggingStateChangedSpy.count() == 1, "loggingStateChanged signal should be emitted");
QVERIFY2(saveLogsChangedSpy.count() == 1, "saveLogsChanged signal should be emitted");
QVERIFY2(m_coreController->m_settingsController->isLoggingEnabled() == !initialLogging, "Logging state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isLoggingEnabled() == !initialLogging, "Logging state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isSaveLogs() == !initialLogging, "Logging state should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->toggleLogging(initialLogging);
QVERIFY2(loggingStateChangedSpy.count() == 2, "loggingStateChanged signal should be emitted again");
QVERIFY2(saveLogsChangedSpy.count() == 2, "saveLogsChanged signal should be emitted again");
QVERIFY2(m_coreController->m_settingsUiController->isLoggingEnabled() == initialLogging, "Logging state should be restored in SettingsUiController");
}
void testScreenshotsSignals() {
QSignalSpy screenshotsEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged);
bool initialScreenshots = m_coreController->m_settingsController->isScreenshotsEnabled();
m_coreController->m_settingsUiController->toggleScreenshotsEnabled(!initialScreenshots);
QVERIFY2(screenshotsEnabledChangedSpy.count() == 1, "screenshotsEnabledChanged signal should be emitted");
QVERIFY2(screenshotsEnabledChangedSpy.at(0).at(0).toBool() == !initialScreenshots, "screenshotsEnabledChanged should emit correct value");
QVERIFY2(m_coreController->m_settingsController->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isScreenshotsEnabled() == !initialScreenshots, "Screenshots state should be available in SecureAppSettingsRepository");
}
void testStartMinimizedSignals() {
QSignalSpy startMinimizedChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::startMinimizedChanged);
bool initialStartMinimized = m_coreController->m_settingsController->isStartMinimizedEnabled();
// AutoStart should be enabled else isStartMinimizedEnabled will return false
m_coreController->m_settingsUiController->toggleAutoStart(true);
m_coreController->m_settingsUiController->toggleStartMinimized(!initialStartMinimized);
QVERIFY2(startMinimizedChangedSpy.count() == 2, "startMinimizedChanged signal should be emitted 2 times: on autoStart and StartMinimized");
QVERIFY2(m_coreController->m_settingsController->isStartMinimizedEnabled() == !initialStartMinimized, "Start minimized state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isStartMinimizedEnabled() == !initialStartMinimized, "Start minimized state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isStartMinimized() == !initialStartMinimized, "Start minimized state should be available in SecureAppSettingsRepository");
}
void testAutoConnectSignals() {
bool initialAutoConnect = m_coreController->m_settingsController->isAutoConnectEnabled();
m_coreController->m_settingsUiController->toggleAutoConnect(!initialAutoConnect);
QVERIFY2(m_coreController->m_settingsController->isAutoConnectEnabled() == !initialAutoConnect, "Auto connect state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isAutoConnectEnabled() == !initialAutoConnect, "Auto connect state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isAutoConnect() == !initialAutoConnect, "Auto connect state should be available in SecureAppSettingsRepository");
m_coreController->m_settingsUiController->toggleAutoConnect(initialAutoConnect);
QVERIFY2(m_coreController->m_settingsController->isAutoConnectEnabled() == initialAutoConnect, "Auto connect state should be restored in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isAutoConnectEnabled() == initialAutoConnect, "Auto connect state should be restored in SettingsUiController");
}
void testLanguageChangeSignals() {
QSignalSpy appLanguageChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appLanguageChanged);
QSignalSpy translationsUpdatedSpy(m_coreController->m_languageUiController, &LanguageUiController::translationsUpdated);
QLocale initialLocale = m_coreController->m_settingsController->getAppLanguage();
QLocale newLocale = (initialLocale.language() == QLocale::English) ? QLocale::Russian : QLocale::English;
m_coreController->m_settingsController->setAppLanguage(newLocale);
QVERIFY2(appLanguageChangedSpy.count() == 1, "appLanguageChanged signal should be emitted");
QVERIFY2(appLanguageChangedSpy.at(0).at(0).value<QLocale>() == newLocale, "appLanguageChanged should emit correct locale");
QVERIFY2(m_coreController->m_settingsController->getAppLanguage() == newLocale, "App language should be updated in SettingsController");
QVERIFY2(m_coreController->m_appSettingsRepository->getAppLanguage() == newLocale, "App language should be available in SecureAppSettingsRepository");
if (m_coreController->m_languageModel) {
QString newLanguageName = m_coreController->m_languageUiController->getCurrentLanguageName();
QVERIFY2(!newLanguageName.isEmpty(), "Language name should be available in LanguageUiController");
}
}
void testGatewayEndpointSignals() {
QSignalSpy gatewayEndpointChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::gatewayEndpointChanged);
QSignalSpy devGatewayEnvChangedSpy(m_coreController->m_settingsUiController, &SettingsUiController::devGatewayEnvChanged);
QString initialEndpoint = m_coreController->m_settingsController->getGatewayEndpoint();
QString newEndpoint = "https://test-gateway.example.com";
m_coreController->m_settingsUiController->setGatewayEndpoint(newEndpoint);
QVERIFY2(gatewayEndpointChangedSpy.count() == 1, "gatewayEndpointChanged signal should be emitted");
QVERIFY2(gatewayEndpointChangedSpy.at(0).at(0).toString() == newEndpoint, "gatewayEndpointChanged should emit correct endpoint");
QVERIFY2(m_coreController->m_settingsController->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->getGatewayEndpoint() == newEndpoint, "Gateway endpoint should be available in SecureAppSettingsRepository");
bool initialDevEnv = m_coreController->m_settingsController->isDevGatewayEnv();
m_coreController->m_settingsUiController->toggleDevGatewayEnv(!initialDevEnv);
QVERIFY2(devGatewayEnvChangedSpy.count() == 1, "devGatewayEnvChanged signal should be emitted");
QVERIFY2(devGatewayEnvChangedSpy.at(0).at(0).toBool() == !initialDevEnv, "devGatewayEnvChanged should emit correct value");
QVERIFY2(m_coreController->m_settingsController->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be updated in SettingsController");
QVERIFY2(m_coreController->m_settingsUiController->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be available in SettingsUiController");
QVERIFY2(m_coreController->m_appSettingsRepository->isDevGatewayEnv() == !initialDevEnv, "Dev gateway env state should be available in SecureAppSettingsRepository");
}
void testSettingsClearedSignal() {
QSignalSpy settingsClearedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::settingsCleared);
m_coreController->m_settingsController->clearSettings();
QVERIFY2(settingsClearedSpy.count() == 1, "settingsCleared signal should be emitted");
}
void testSplitTunnelingSignals() {
QSignalSpy siteSplitTunnelingToggledSpy(m_coreController->m_settingsController, &SettingsController::siteSplitTunnelingToggled);
QSignalSpy appSplitTunnelingToggledSpy(m_coreController->m_settingsController, &SettingsController::appSplitTunnelingToggled);
QSignalSpy sitesSplitTunnelingEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::sitesSplitTunnelingEnabledChanged);
QSignalSpy appsSplitTunnelingEnabledChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsSplitTunnelingEnabledChanged);
QSignalSpy routeModeChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::routeModeChanged);
QSignalSpy appsRouteModeChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsRouteModeChanged);
QSignalSpy sitesChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::sitesChanged);
QSignalSpy appsChangedSpy(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::appsChanged);
bool initialSitesSplitTunneling = m_coreController->m_ipSplitTunnelingController->isSplitTunnelingEnabled();
m_coreController->m_ipSplitTunnelingController->toggleSplitTunneling(!initialSitesSplitTunneling);
QVERIFY2(sitesSplitTunnelingEnabledChangedSpy.count() == 1, "sitesSplitTunnelingEnabledChanged signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingController->isSplitTunnelingEnabled() == !initialSitesSplitTunneling, "Sites split tunneling should be updated in IpSplitTunnelingController");
QVERIFY2(m_coreController->m_appSettingsRepository->isSitesSplitTunnelingEnabled() == !initialSitesSplitTunneling, "Sites split tunneling should be available in SecureAppSettingsRepository");
bool initialAppsSplitTunneling = m_coreController->m_appSplitTunnelingController->isSplitTunnelingEnabled();
m_coreController->m_appSplitTunnelingController->toggleSplitTunneling(!initialAppsSplitTunneling);
QVERIFY2(appsSplitTunnelingEnabledChangedSpy.count() == 1, "appsSplitTunnelingEnabledChanged signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingController->isSplitTunnelingEnabled() == !initialAppsSplitTunneling, "Apps split tunneling should be updated in AppSplitTunnelingController");
QVERIFY2(m_coreController->m_appSettingsRepository->isAppsSplitTunnelingEnabled() == !initialAppsSplitTunneling, "Apps split tunneling should be available in SecureAppSettingsRepository");
RouteMode initialRouteMode = m_coreController->m_ipSplitTunnelingController->getRouteMode();
RouteMode newRouteMode = (initialRouteMode == RouteMode::VpnOnlyForwardSites)
? RouteMode::VpnAllExceptSites
: RouteMode::VpnOnlyForwardSites;
m_coreController->m_ipSplitTunnelingController->setRouteMode(newRouteMode);
QVERIFY2(routeModeChangedSpy.count() == 1, "routeModeChanged signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingController->getRouteMode() == newRouteMode, "Route mode should be updated in IpSplitTunnelingController");
QVERIFY2(m_coreController->m_appSettingsRepository->routeMode() == newRouteMode, "Route mode should be available in SecureAppSettingsRepository");
AppsRouteMode initialAppsRouteMode = m_coreController->m_appSplitTunnelingController->getRouteMode();
AppsRouteMode newAppsRouteMode = (initialAppsRouteMode == AppsRouteMode::VpnAllExceptApps)
? AppsRouteMode::VpnAllApps
: AppsRouteMode::VpnAllExceptApps;
m_coreController->m_appSplitTunnelingController->setRouteMode(newAppsRouteMode);
QVERIFY2(appsRouteModeChangedSpy.count() == 1, "appsRouteModeChanged signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingController->getRouteMode() == newAppsRouteMode, "Apps route mode should be updated in AppSplitTunnelingController");
QVERIFY2(m_coreController->m_appSettingsRepository->appsRouteMode() == newAppsRouteMode, "Apps route mode should be available in SecureAppSettingsRepository");
QMap<QString, QStringList> sitesMap{{"example.com", QStringList{"1.2.3.4"}}};
m_coreController->m_ipSplitTunnelingController->addSites(sitesMap, true);
QVERIFY2(sitesChangedSpy.count() >= 1, "sitesChanged signal should be emitted");
QVector<QPair<QString, QStringList>> currentSites = m_coreController->m_ipSplitTunnelingController->getCurrentSites();
QVERIFY2(currentSites.size() >= 1, "Sites should be available in IpSplitTunnelingController");
QVERIFY2(m_coreController->m_ipSplitTunnelingUiController != nullptr, "IpSplitTunnelingUiController should exist");
QVERIFY2(m_coreController->m_ipSplitTunnelingModel != nullptr, "IpSplitTunnelingModel should exist");
m_coreController->m_ipSplitTunnelingUiController->updateModel();
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() >= 1, "Sites should be available in IpSplitTunnelingModel");
QString modelUrl = m_coreController->m_ipSplitTunnelingModel->data(m_coreController->m_ipSplitTunnelingModel->index(0, 0), IpSplitTunnelingModel::UrlRole).toString();
QVERIFY2(modelUrl == "example.com", "Site URL should be available in IpSplitTunnelingModel");
}
};
QTEST_MAIN(TestSettingsSignals)
#include "testSettingsSignals.moc"
+98
View File
@@ -0,0 +1,98 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QSignalSpy>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
#include "secureQSettings.h"
using namespace amnezia;
using namespace amnezia::test;
class TestSignalOrder : public QObject
{
Q_OBJECT
private:
TestCoreController* m_coreController;
SecureQSettings* m_settings;
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
m_coreController->m_serversRepository->invalidateCache();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testSignalOrderOnImport() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
if (!isEnvValueConfigured(awgKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY");
}
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QSignalSpy serverAddedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverAdded);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
auto importResult = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult.config);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
QVERIFY2(serverAddedSpy.count() == 1, "serverAdded signal should be emitted");
QVERIFY2(defaultServerChangedSpy.count() == 0, "defaultServerChanged signal should NOT be emitted (default is already 0)");
QVERIFY2(serverAddedSpy.at(0).count() > 0, "serverAdded should have arguments");
}
void testSignalOrderOnRemoveDefault() {
QString awgKey = getEnvValue("THIRD_PARTY_AWG_VPN_KEY");
QString xrayKey = getEnvValue("THIRD_PARTY_XRAY_VPN_KEY");
if (!isEnvValueConfigured(awgKey) || !isEnvValueConfigured(xrayKey)) {
QSKIP("Set THIRD_PARTY_AWG_VPN_KEY and THIRD_PARTY_XRAY_VPN_KEY");
}
auto importResult1 = m_coreController->m_importCoreController->extractConfigFromData(awgKey);
m_coreController->m_importCoreController->importConfig(importResult1.config);
auto importResult2 = m_coreController->m_importCoreController->extractConfigFromData(xrayKey);
m_coreController->m_importCoreController->importConfig(importResult2.config);
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 1, "Default should be index 1");
QSignalSpy serverRemovedSpy(m_coreController->m_serversRepository, &SecureServersRepository::serverRemoved);
QSignalSpy defaultServerChangedSpy(m_coreController->m_serversRepository, &SecureServersRepository::defaultServerChanged);
m_coreController->m_serversController->removeServer(m_coreController->m_serversController->getServerId(1));
QVERIFY2(serverRemovedSpy.count() == 1, "serverRemoved signal should be emitted");
QVERIFY2(defaultServerChangedSpy.count() == 1, "defaultServerChanged signal should be emitted when removing default server");
QVERIFY2(defaultServerChangedSpy.at(0).at(0).toString() == m_coreController->m_serversRepository->defaultServerId(),
"defaultServerChanged should emit new default server id");
QVERIFY2(m_coreController->m_serversRepository->defaultServerIndex() == 0, "Default server index should be 0");
}
};
QTEST_MAIN(TestSignalOrder)
#include "testSignalOrder.moc"
@@ -0,0 +1,107 @@
#include <QDebug>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QUuid>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
class TestUiAllowedDnsModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
private slots:
void initTestCase()
{
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase()
{
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init()
{
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testRolesAndSignals()
{
QSignalSpy finishedSpy(m_coreController->m_allowedDnsUiController, &AllowedDnsUiController::finished);
QSignalSpy errorOccurredSpy(m_coreController->m_allowedDnsUiController, &AllowedDnsUiController::errorOccurred);
QString ip = "188.40.167.81";
m_coreController->m_allowedDnsUiController->addDns(ip);
m_coreController->m_allowedDnsUiController->updateModel();
QVERIFY2(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QVERIFY2(finishedSpy.count() == 1, "finished signal should be emitted");
QVERIFY2(m_coreController->m_allowedDnsModel->rowCount() == 1, "AllowedDnsModel should have 1 row");
QModelIndex allowedDnsModelIndex = m_coreController->m_allowedDnsModel->index(0, 0);
QVERIFY2(allowedDnsModelIndex.isValid(), "Site model index should be valid");
auto dnsIp = m_coreController->m_allowedDnsModel->data(allowedDnsModelIndex, AllowedDnsModel::IpRole);
QString msg = QString("dns ip should be %1, got %2").arg(ip, dnsIp.toString());
QVERIFY2(dnsIp == ip, msg.toLocal8Bit().constData());
QTemporaryDir tempDir;
QVERIFY2(tempDir.isValid(), "Temporary directory should be created");
const QString dnsFile = tempDir.filePath(QStringLiteral("dns.json"));
{
QFile file(dnsFile);
QVERIFY2(file.open(QIODevice::WriteOnly), "DNS import file should be writable");
file.write(R"(["8.8.8.8", "1.1.1.1"])");
}
m_coreController->m_allowedDnsUiController->importDns(dnsFile, true);
m_coreController->m_allowedDnsUiController->updateModel();
QVERIFY2(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QVERIFY2(finishedSpy.count() == 2, "finished signal should be emitted");
QVERIFY2(m_coreController->m_allowedDnsModel->rowCount() > 1, "AllowedDnsModel should have more than 1 row");
const QString exportFile = tempDir.filePath(QStringLiteral("test_dns_export.json"));
m_coreController->m_allowedDnsUiController->exportDns(exportFile);
QVERIFY2(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QVERIFY2(finishedSpy.count() == 3, "finished signal should be emitted");
m_coreController->m_allowedDnsUiController->removeDns(0);
m_coreController->m_allowedDnsUiController->updateModel();
QVERIFY2(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QVERIFY2(finishedSpy.count() == 4, "finished signal should be emitted");
QVERIFY2(m_coreController->m_allowedDnsModel->rowCount() == 1, "AllowedDnsModel should have 1 row after removing first DNS");
m_coreController->m_allowedDnsUiController->removeDns(0);
m_coreController->m_allowedDnsUiController->updateModel();
QVERIFY2(finishedSpy.count() == 5, "finished signal should be emitted");
QVERIFY2(m_coreController->m_allowedDnsModel->rowCount() == 0, "AllowedDnsModel should have 0 rows");
}
};
QTEST_MAIN(TestUiAllowedDnsModelAndController)
#include "testUiAllowedDnsModelAndController.moc"
@@ -0,0 +1,100 @@
#include <QCoreApplication>
#include <QDebug>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QUuid>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
class TestUiAppSTModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
private slots:
void initTestCase()
{
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase()
{
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init()
{
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testRolesAndSignals()
{
QSignalSpy finishedSpy(m_coreController->m_appSplitTunnelingUiController, &AppSplitTunnelingUiController::finished);
QSignalSpy errorOccurredSpy(m_coreController->m_appSplitTunnelingUiController, &AppSplitTunnelingUiController::errorOccurred);
QSignalSpy isSplitTunnelingChangedSpy(m_coreController->m_appSplitTunnelingUiController, &AppSplitTunnelingUiController::isSplitTunnelingEnabledChanged);
m_coreController->m_appSplitTunnelingUiController->toggleSplitTunneling(true);
QVERIFY2(isSplitTunnelingChangedSpy.count() == 1, "isSplitTunnelingChangedSpy signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingUiController->isSplitTunnelingEnabled() == true, "AppSplitTunneling should be enabled");
m_coreController->m_appSplitTunnelingUiController->toggleSplitTunneling(false);
QVERIFY2(isSplitTunnelingChangedSpy.count() == 2, "isSplitTunnelingChangedSpy signal should be emitted 2nd time");
QVERIFY2(m_coreController->m_appSplitTunnelingUiController->isSplitTunnelingEnabled() == false, "AppSplitTunneling should be disabled");
const QString app = QCoreApplication::applicationFilePath();
m_coreController->m_appSplitTunnelingUiController->addApp(app);
m_coreController->m_appSplitTunnelingUiController->updateModel();
QVERIFY2(finishedSpy.count() == 1, "finished signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingModel->rowCount() == 1, "AppSplitTunnelingModel should have 1 row");
QModelIndex appSTModelIndex = m_coreController->m_appSplitTunnelingModel->index(0, 0);
QVERIFY2(appSTModelIndex.isValid(), "Site model index should be valid");
auto appPath = m_coreController->m_appSplitTunnelingModel->data(appSTModelIndex, AppSplitTunnelingModel::AppPathRole);
QString msg = QString("app path should be %1, got %2").arg(app, appPath.toString());
QVERIFY2(app.contains(appPath.toString()) == true, msg.toLocal8Bit().constData());
auto pkgAppName = m_coreController->m_appSplitTunnelingModel->data(appSTModelIndex, AppSplitTunnelingModel::PackageAppNameRole);
QVERIFY2(pkgAppName == true, "app name should be set");
auto pkgAppIcon = m_coreController->m_appSplitTunnelingModel->data(appSTModelIndex, AppSplitTunnelingModel::PackageAppIconRole);
QVERIFY2(pkgAppIcon == true, "app image should be set");
m_coreController->m_appSplitTunnelingUiController->addApp(app);
m_coreController->m_appSplitTunnelingUiController->updateModel();
QVERIFY2(errorOccurredSpy.count() == 1, "errorOccurred signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingModel->rowCount() == 1, "AppSplitTunnelingModel should have 3 rows (same app should not be added)");
m_coreController->m_appSplitTunnelingUiController->removeApp(0);
m_coreController->m_appSplitTunnelingUiController->updateModel();
QVERIFY2(finishedSpy.count() == 2, "finished signal should be emitted");
QVERIFY2(m_coreController->m_appSplitTunnelingModel->rowCount() == 0, "AppSplitTunnelingModel should have 0 rows");
}
};
QTEST_MAIN(TestUiAppSTModelAndController)
#include "testUiAppSTModelAndController.moc"
+129
View File
@@ -0,0 +1,129 @@
#include <QDebug>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QUuid>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
class TestUiIpModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
QString normalizeHostname(const QString &hostname) const
{
QString normalized = hostname;
normalized.replace("https://", "");
normalized.replace("http://", "");
normalized.replace("ftp://", "");
normalized = normalized.split("/", Qt::SkipEmptyParts).first();
return normalized;
}
private slots:
void initTestCase()
{
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase()
{
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init()
{
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testRolesAndSignals()
{
QSignalSpy finishedSpy(m_coreController->m_ipSplitTunnelingUiController, &IpSplitTunnelingUiController::finished);
QSignalSpy errorOccurredSpy(m_coreController->m_ipSplitTunnelingUiController, &IpSplitTunnelingUiController::errorOccurred);
QSignalSpy isSplitTunnelingChangedSpy(m_coreController->m_ipSplitTunnelingUiController, &IpSplitTunnelingUiController::isSplitTunnelingEnabledChanged);
m_coreController->m_ipSplitTunnelingUiController->toggleSplitTunneling(true);
QVERIFY2(isSplitTunnelingChangedSpy.count() == 1, "isSplitTunnelingChangedSpy signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingUiController->isSplitTunnelingEnabled() == true, "ipSplitTunneling should be enabled");
m_coreController->m_ipSplitTunnelingUiController->toggleSplitTunneling(false);
QVERIFY2(isSplitTunnelingChangedSpy.count() == 2, "isSplitTunnelingChangedSpy signal should be emitted 2nd time");
QVERIFY2(m_coreController->m_ipSplitTunnelingUiController->isSplitTunnelingEnabled() == false, "ipSplitTunneling should be disabled");
QString site = "2ip.io";
m_coreController->m_ipSplitTunnelingUiController->addSite(site);
m_coreController->m_ipSplitTunnelingUiController->addSite("whatismyipaddress.com");
m_coreController->m_ipSplitTunnelingUiController->updateModel();
QVERIFY2(finishedSpy.count() == 2, "finished signal should be emitted 2 times");
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() == 2, "IpSplitTunnelingModel should have 2 rows");
QModelIndex siteModelIndex = m_coreController->m_ipSplitTunnelingModel->index(0, 0);
QVERIFY2(siteModelIndex.isValid(), "Ip model index should be valid");
auto siteUrl = m_coreController->m_ipSplitTunnelingModel->data(siteModelIndex, IpSplitTunnelingModel::UrlRole);
QCOMPARE(siteUrl, normalizeHostname(site));
auto siteIp = m_coreController->m_ipSplitTunnelingModel->data(siteModelIndex, IpSplitTunnelingModel::IpRole);
QVERIFY2(siteIp.isNull() == false, "Ip should not be empty");
m_coreController->m_ipSplitTunnelingUiController->removeSite(0);
m_coreController->m_ipSplitTunnelingUiController->updateModel();
QVERIFY2(finishedSpy.count() == 3, "finished signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() == 1, "IpSplitTunnelingModel should have 1 row");
QTemporaryDir tempDir;
QVERIFY2(tempDir.isValid(), "Temporary directory should be created");
const QString sitesFile = tempDir.filePath(QStringLiteral("sites.json"));
{
QFile file(sitesFile);
QVERIFY2(file.open(QIODevice::WriteOnly), "Sites import file should be writable");
file.write(R"([
{"hostname": "example.com", "ip": "93.184.216.34"},
{"hostname": "test.org", "ip": "1.2.3.4"}
])");
}
m_coreController->m_ipSplitTunnelingUiController->importSites(sitesFile, true);
m_coreController->m_ipSplitTunnelingUiController->updateModel();
QVERIFY2(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
QVERIFY2(finishedSpy.count() == 4, "finished signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() > 1, "IpSplitTunnelingModel should have more than 1 row");
const QString exportFile = tempDir.filePath(QStringLiteral("test_ips_export.json"));
m_coreController->m_ipSplitTunnelingUiController->exportSites(exportFile);
QVERIFY2(finishedSpy.count() == 5, "finished signal should be emitted");
m_coreController->m_ipSplitTunnelingUiController->removeSites();
m_coreController->m_ipSplitTunnelingUiController->updateModel();
QVERIFY2(finishedSpy.count() == 6, "finished signal should be emitted");
QVERIFY2(m_coreController->m_ipSplitTunnelingModel->rowCount() == 0, "IpSplitTunnelingModel should have 0 rows");
}
};
QTEST_MAIN(TestUiIpModelAndController)
#include "testUiIpModelAndController.moc"
@@ -0,0 +1,91 @@
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QUuid>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "secureQSettings.h"
#include "vpnConnection.h"
using namespace amnezia;
class TestUiLanguageModelAndController : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
private slots:
void initTestCase()
{
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase()
{
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init()
{
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testChangeLanguage()
{
QVERIFY2(m_coreController->m_languageModel->rowCount() > 0, "Language model should not be empty");
QSignalSpy updateTranslationsSpy(m_coreController->m_languageUiController, &LanguageUiController::updateTranslations);
QSignalSpy translationsUpdatedSpy(m_coreController->m_languageUiController, &LanguageUiController::translationsUpdated);
m_coreController->m_languageUiController->changeLanguage(LanguageSettings::AvailableLanguageEnum::China_cn);
QVERIFY2(updateTranslationsSpy.count() == 1, "updateTranslations signal should be emitted");
QVERIFY2(translationsUpdatedSpy.count() == 1, "translationsUpdated signal should be emitted");
m_coreController->m_languageUiController->changeLanguage(LanguageSettings::AvailableLanguageEnum::English);
QVERIFY2(updateTranslationsSpy.count() == 2, "updateTranslations signal should be emitted");
QVERIFY2(translationsUpdatedSpy.count() == 2, "translationsUpdated signal should be emitted");
}
void testUrl()
{
m_coreController->m_languageUiController->changeLanguage(LanguageSettings::AvailableLanguageEnum::Russian);
QString siteRU = m_coreController->m_languageUiController->getCurrentSiteUrl("test_path");
QString docsRU = m_coreController->m_languageUiController->getCurrentDocsUrl("test_path");
m_coreController->m_languageUiController->changeLanguage(LanguageSettings::AvailableLanguageEnum::English);
QString siteEN = m_coreController->m_languageUiController->getCurrentSiteUrl("test_path");
QString docsEN = m_coreController->m_languageUiController->getCurrentDocsUrl("test_path");
QVERIFY2(siteRU != siteEN, "site url's should not be same");
QVERIFY2(docsRU != docsEN, "docs url's should not be same");
}
void testLineHeight()
{
m_coreController->m_languageUiController->changeLanguage(LanguageSettings::AvailableLanguageEnum::Burmese);
QVERIFY2(m_coreController->m_languageUiController->getLineHeightAppend() == 10, "line height should be 10");
m_coreController->m_languageUiController->changeLanguage(LanguageSettings::AvailableLanguageEnum::English);
QVERIFY2(m_coreController->m_languageUiController->getLineHeightAppend() == 0, "line height should be 0");
}
};
QTEST_MAIN(TestUiLanguageModelAndController)
#include "testUiLanguageModelAndController.moc"
@@ -0,0 +1,314 @@
#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::initPacketJunkSize] = QString::number(protocols::awg::initPacketJunkSizeMax);
clientConfig[configKey::responsePacketJunkSize] = QString::number(protocols::awg::responsePacketJunkSizeMax);
clientConfig[configKey::cookieReplyPacketJunkSize] = QString::number(protocols::awg::cookieReplyPacketJunkSizeMax);
clientConfig[configKey::transportPacketJunkSize] = protocols::awg::defaultPadding;
clientConfig[configKey::initPacketMagicHeader] = protocols::awg::defaultInitPacketMagicHeader;
clientConfig[configKey::responsePacketMagicHeader] = protocols::awg::defaultResponsePacketMagicHeader;
clientConfig[configKey::underloadPacketMagicHeader] = protocols::awg::defaultUnderloadPacketMagicHeader;
clientConfig[configKey::transportPacketMagicHeader] = protocols::awg::defaultTransportPacketMagicHeader;
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] = QString::number(protocols::awg::initPacketJunkSizeMax);
awgConfig[configKey::responsePacketJunkSize] = QString::number(protocols::awg::responsePacketJunkSizeMax);
awgConfig[configKey::cookieReplyPacketJunkSize] = QString::number(protocols::awg::cookieReplyPacketJunkSizeMax);
awgConfig[configKey::transportPacketJunkSize] = protocols::awg::defaultPadding;
awgConfig[configKey::initPacketMagicHeader] = protocols::awg::defaultInitPacketMagicHeader;
awgConfig[configKey::responsePacketMagicHeader] = protocols::awg::defaultResponsePacketMagicHeader;
awgConfig[configKey::underloadPacketMagicHeader] = protocols::awg::defaultUnderloadPacketMagicHeader;
awgConfig[configKey::transportPacketMagicHeader] = protocols::awg::defaultTransportPacketMagicHeader;
awgConfig[configKey::specialJunk1] = protocols::awg::defaultSpecialJunk1;
awgConfig[configKey::specialJunk2] = protocols::awg::defaultSpecialJunk2;
awgConfig[configKey::specialJunk3] = protocols::awg::defaultSpecialJunk3;
awgConfig[configKey::specialJunk4] = protocols::awg::defaultSpecialJunk4;
awgConfig[configKey::specialJunk5] = protocols::awg::defaultSpecialJunk5;
awgConfig[configKey::isThirdPartyConfig] = true;
QJsonObject container;
container[configKey::container] = "amnezia-awg";
container[configKey::awg] = awgConfig;
QJsonArray containers;
containers.append(container);
QJsonObject config;
config[configKey::containers] = containers;
config[configKey::defaultContainer] = "amnezia-awg";
config[configKey::description] = "AWG2 Test Server";
config[configKey::hostName] = "test.example.com";
return config;
}
QJsonObject createServerDescriptionTestConfig(bool withAmneziaDns)
{
QJsonObject config = createAwg2Config();
config[configKey::description] = "Server 1";
if (withAmneziaDns) {
config[configKey::dns1] = protocols::dns::amneziaDnsIp;
}
return config;
}
private slots:
void initTestCase() {
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase() {
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init() {
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testUiServersModelAndControllerRoles() {
QJsonObject testConfig = createAwg2Config();
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
m_coreController->m_importCoreController->importConfig(testConfig);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished signal should be emitted");
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server should be imported");
int serverIndex = m_coreController->m_serversRepository->defaultServerIndex();
QVERIFY2(serverIndex == 0, "Default server index should be 0");
if (m_coreController->m_serversModel) {
QVERIFY2(m_coreController->m_serversModel->rowCount() == 1, "ServersModel should have 1 row");
QModelIndex serverModelIndex = m_coreController->m_serversModel->index(0, 0);
QVERIFY2(serverModelIndex.isValid(), "Server model index should be valid");
QString serverName = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::NameRole).toString();
QVERIFY2(serverName == "AWG2 Test Server", QString("Server name should be 'AWG2 Test Server', got '%1'").arg(serverName).toUtf8().constData());
QString serverDescription = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::ServerDescriptionRole).toString();
QVERIFY2(serverDescription.contains("test.example.com"), QString("Server description should contain hostname, got '%1'").arg(serverDescription).toUtf8().constData());
QString hostName = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HostNameRole).toString();
QVERIFY2(hostName == "test.example.com", "Host name should match");
bool isDefault = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::IsDefaultRole).toBool();
QVERIFY2(isDefault == true, "Server should be default");
bool hasInstalledContainers = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HasInstalledContainers).toBool();
QVERIFY2(hasInstalledContainers == true, "Server should have installed containers");
bool hasWriteAccess = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::HasWriteAccessRole).toBool();
QVERIFY2(hasWriteAccess == false, "Server should not have write access for imported config");
int defaultContainerRole = m_coreController->m_serversModel->data(serverModelIndex, ServersModel::DefaultContainerRole).toInt();
DockerContainer expectedContainer = DockerContainer::Awg;
QVERIFY2(defaultContainerRole == static_cast<int>(expectedContainer), "Default container should be Awg");
}
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(
m_coreController->m_serversUiController->getServerId(0));
QString hostName = "test.example.com";
QString collapsedDescription = m_coreController->m_serversUiController->getDefaultServerDescriptionCollapsed();
QString expectedCollapsed = "AmneziaWG (version 2) | " + hostName;
QVERIFY2(collapsedDescription == expectedCollapsed,
QString("Collapsed description should be '%1', got '%2'").arg(expectedCollapsed, collapsedDescription).toUtf8().constData());
QString expandedDescription = m_coreController->m_serversUiController->getDefaultServerDescriptionExpanded();
QString expectedExpanded = hostName;
QVERIFY2(expandedDescription == expectedExpanded,
QString("Expanded description should be '%1', got '%2'").arg(expectedExpanded, expandedDescription).toUtf8().constData());
}
if (m_coreController->m_containersModel) {
int awgContainerIndex = -1;
for (int i = 0; i < ContainerUtils::allContainers().size(); ++i) {
DockerContainer container = ContainerUtils::allContainers().at(i);
if (container == DockerContainer::Awg) {
awgContainerIndex = i;
break;
}
}
QVERIFY2(awgContainerIndex >= 0, "Awg container index should be found");
QModelIndex containerModelIndex = m_coreController->m_containersModel->index(awgContainerIndex, 0);
QVERIFY2(containerModelIndex.isValid(), "Container model index should be valid");
bool isInstalled = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsInstalledRole).toBool();
QVERIFY2(isInstalled == true, "Awg container should be installed");
bool isVpnContainer = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsVpnContainerRole).toBool();
QVERIFY2(isVpnContainer == true, "Awg container should be VPN container");
QString containerName = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::NameRole).toString();
QString expectedContainerName = ContainerUtils::containerHumanNames().value(DockerContainer::Awg);
QVERIFY2(containerName == expectedContainerName, QString("Container name should be '%1', got '%2'").arg(expectedContainerName, containerName).toUtf8().constData());
QString containerDescription = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DescriptionRole).toString();
QString expectedDescription = ContainerUtils::containerDescriptions().value(DockerContainer::Awg);
QVERIFY2(containerDescription == expectedDescription, QString("Container description should match, got '%1'").arg(containerDescription).toUtf8().constData());
QString detailedDescription = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DetailedDescriptionRole).toString();
QString expectedDetailedDescription = ContainerUtils::containerDetailedDescriptions().value(DockerContainer::Awg);
QVERIFY2(detailedDescription == expectedDetailedDescription, QString("Container detailed description should match, got '%1'").arg(detailedDescription).toUtf8().constData());
int serviceType = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ServiceTypeRole).toInt();
QVERIFY2(serviceType == static_cast<int>(ProtocolEnumNS::ServiceType::Vpn), "Service type should be Vpn");
bool isSupported = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsSupportedRole).toBool();
QVERIFY2(isSupported == true, "Container should be supported");
bool isShareable = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsShareableRole).toBool();
QVERIFY2(isShareable == true, "Container should be shareable");
QJsonObject containerConfig = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ConfigRole).toJsonObject();
QVERIFY2(!containerConfig.isEmpty(), "Container config should not be empty");
QVERIFY2(containerConfig.value(configKey::container).toString() == "amnezia-awg", "Container config should have correct container type");
QJsonObject awgProtocolConfig = containerConfig.value(configKey::awg).toObject();
QVERIFY2(!awgProtocolConfig.isEmpty(), "AWG protocol config should not be empty");
QString protocolVersion = awgProtocolConfig.value(configKey::protocolVersion).toString();
QVERIFY2(protocolVersion == protocols::awg::awgV2, QString("Protocol version should be '%1', got '%2'").arg(protocols::awg::awgV2, protocolVersion).toUtf8().constData());
QString port = awgProtocolConfig.value(configKey::port).toString();
QVERIFY2(port == protocols::awg::defaultPort, QString("Port should be '%1', got '%2'").arg(protocols::awg::defaultPort, port).toUtf8().constData());
QString subnetAddress = awgProtocolConfig.value(configKey::subnetAddress).toString();
QVERIFY2(subnetAddress == protocols::wireguard::defaultSubnetAddress, QString("Subnet address should be '%1', got '%2'").arg(protocols::wireguard::defaultSubnetAddress, subnetAddress).toUtf8().constData());
bool isThirdParty = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::IsThirdPartyConfigRole).toBool();
QVERIFY2(isThirdParty == true, "Imported config should be third party config");
DockerContainer dockerContainer = static_cast<DockerContainer>(m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::DockerContainerRole).toInt());
QVERIFY2(dockerContainer == DockerContainer::Awg, "Docker container should be Awg");
QString containerString = m_coreController->m_containersModel->data(containerModelIndex, ContainersModel::ContainerStringRole).toString();
QVERIFY2(containerString == "amnezia-awg", "Container string should be amnezia-awg");
}
}
void testServerDescriptionFormat() {
QSignalSpy importFinishedSpy(m_coreController->m_importCoreController, &ImportController::importFinished);
QJsonObject configNoDns = createServerDescriptionTestConfig(false);
m_coreController->m_importCoreController->importConfig(configNoDns);
QVERIFY2(importFinishedSpy.count() == 1, "importFinished should be emitted");
m_coreController->m_appSettingsRepository->setUseAmneziaDns(false);
QVector<ServerDescription> descriptionsNoDns = m_coreController->m_serversController->buildServerDescriptions(
m_coreController->m_appSettingsRepository->useAmneziaDns());
const QString defIdNoDns = m_coreController->m_serversRepository->defaultServerId();
m_coreController->m_serversModel->updateModel(descriptionsNoDns, defIdNoDns);
QString descNoDns = m_coreController->m_serversModel->data(
m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString();
QVERIFY2(descNoDns == "test.example.com",
QString("Without Amnezia DNS expected 'test.example.com', got '%1'").arg(descNoDns).toUtf8().constData());
m_coreController->m_serversRepository->clearServers();
if (m_coreController->m_serversRepository->serversCount() > 0) {
m_coreController->m_serversRepository->setDefaultServer(m_coreController->m_serversRepository->serverIdAt(0));
}
QJsonObject configWithDns = createServerDescriptionTestConfig(true);
m_coreController->m_importCoreController->importConfig(configWithDns);
QVERIFY2(m_coreController->m_serversRepository->serversCount() == 1, "Server should be imported");
m_coreController->m_appSettingsRepository->setUseAmneziaDns(true);
QVector<ServerDescription> descriptionsWithDns = m_coreController->m_serversController->buildServerDescriptions(
m_coreController->m_appSettingsRepository->useAmneziaDns());
const QString defIdWithDns = m_coreController->m_serversRepository->defaultServerId();
m_coreController->m_serversModel->updateModel(descriptionsWithDns, defIdWithDns);
QString descWithDns = m_coreController->m_serversModel->data(
m_coreController->m_serversModel->index(0, 0), ServersModel::ServerDescriptionRole).toString();
QVERIFY2(descWithDns == "Amnezia DNS | test.example.com",
QString("With Amnezia DNS expected 'Amnezia DNS | test.example.com', got '%1'").arg(descWithDns).toUtf8().constData());
}
};
QTEST_MAIN(TestUiServersModelAndController)
#include "testUiServersModelAndController.moc"
+290
View File
@@ -0,0 +1,290 @@
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <QUuid>
#include <QTest>
#include "utils/testCoreController.h"
#include "core/models/serverDescription.h"
#include "core/utils/serialization/serialization.h"
#include "core/utils/utilities.h"
#include "secureQSettings.h"
#include "utils/testUtils.h"
#include "vpnConnection.h"
using namespace amnezia;
using namespace amnezia::test;
class TestXraySerialization : public QObject
{
Q_OBJECT
private:
TestCoreController *m_coreController;
SecureQSettings *m_settings;
QJsonObject extractXrayConfig(const QString &data, ConfigTypes configType, const QString &description = "") const
{
QJsonParseError parserErr;
QJsonDocument jsonConf = QJsonDocument::fromJson(data.toLocal8Bit(), &parserErr);
QJsonObject xrayVpnConfig;
xrayVpnConfig[configKey::config] = jsonConf.toJson().constData();
QJsonObject lastConfig;
lastConfig[configKey::lastConfig] = jsonConf.toJson().constData();
lastConfig[configKey::isThirdPartyConfig] = true;
QJsonObject containers;
if (configType == ConfigTypes::ShadowSocks) {
containers.insert(configKey::ssxray, QJsonValue(lastConfig));
containers.insert(configKey::container, QJsonValue(configKey::amneziaSsxray));
} else {
containers.insert(configKey::container, QJsonValue(configKey::amneziaXray));
containers.insert(configKey::xray, QJsonValue(lastConfig));
}
QJsonArray arr;
arr.push_back(containers);
QString hostName;
const static QRegularExpression hostNameRegExp("\"address\":\\s*\"([^\"]+)");
QRegularExpressionMatch hostNameMatch = hostNameRegExp.match(data);
if (hostNameMatch.hasMatch()) {
hostName = hostNameMatch.captured(1);
}
QJsonObject config;
config[configKey::containers] = arr;
config[configKey::defaultContainer] =
(configType == ConfigTypes::ShadowSocks) ? configKey::amneziaSsxray : configKey::amneziaXray;
if (description.isEmpty()) {
config[configKey::description] = m_coreController->m_serversRepository->nextAvailableServerName();
} else {
config[configKey::description] = description;
}
config[configKey::hostName] = hostName;
return config;
}
private slots:
void initTestCase()
{
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
}
void cleanupTestCase()
{
m_settings->clearSettings();
delete m_coreController;
delete m_settings;
}
void init()
{
m_settings->clearSettings();
if (m_coreController->m_serversModel) {
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
}
}
void testVless()
{
const QString configData = getEnvValue("THIRD_PARTY_VLESS_IMPORT_DATA");
if (!isEnvValueConfigured(configData)) {
QSKIP("THIRD_PARTY_VLESS_IMPORT_DATA is not configured");
}
ImportController::ImportResult importResult;
m_coreController->m_importCoreController->extractConfigFromData(configData);
QString config = configData;
QString prefix;
QString errormsg;
ConfigTypes configType = ConfigTypes::Invalid;
if (config.startsWith("vless://")) {
configType = ConfigTypes::Xray;
importResult.config = extractXrayConfig(
Utils::JsonToString(serialization::vless::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact),
configType, prefix);
QVERIFY2(!importResult.config.empty(), "Config shouldn't be empty");
} else {
QSKIP("Config not starts with vless://");
}
QCOMPARE(importResult.config, config);
}
void testVmessNew()
{
const QString configData = getEnvValue("THIRD_PARTY_VMESS_NEW_IMPORT_DATA");
if (!isEnvValueConfigured(configData)) {
QSKIP("THIRD_PARTY_VMESS_NEW_IMPORT_DATA is not configured");
}
QString clientName = "Test Client (vmess_new deserialization)";
ImportController::ImportResult importResult;
m_coreController->m_importCoreController->extractConfigFromData(configData);
QString config = configData;
QString prefix;
QString errormsg;
ConfigTypes configType = ConfigTypes::Invalid;
if (config.startsWith("vmess://") && config.contains("@")) {
configType = ConfigTypes::Xray;
importResult.config = extractXrayConfig(
Utils::JsonToString(serialization::vmess_new::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact),
configType, prefix);
QVERIFY2(!importResult.config.empty(), "Config shouldn't be empty");
} else {
QSKIP("Config not starts with vmess:// or not contain @");
}
QCOMPARE(importResult.config, config);
}
void testVmess()
{
const QString configData = getEnvValue("THIRD_PARTY_VMESS_IMPORT_DATA");
if (!isEnvValueConfigured(configData)) {
QSKIP("THIRD_PARTY_VMESS_IMPORT_DATA is not configured");
}
QString clientName = "Test Client (vmess deserialization)";
ImportController::ImportResult importResult;
m_coreController->m_importCoreController->extractConfigFromData(configData);
QString config = configData;
QString prefix;
QString errormsg;
ConfigTypes configType = ConfigTypes::Invalid;
if (config.startsWith("vmess://")) {
configType = ConfigTypes::Xray;
importResult.config = extractXrayConfig(
Utils::JsonToString(serialization::vmess::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact),
configType, prefix);
QVERIFY2(!importResult.config.empty(), "Config shouldn't be empty");
} else {
QSKIP("Config not starts with vmess://");
}
QCOMPARE(importResult.config, config);
}
void testTrojan()
{
const QString configData = getEnvValue("THIRD_PARTY_TROJAN_IMPORT_DATA");
if (!isEnvValueConfigured(configData)) {
QSKIP("THIRD_PARTY_TROJAN_IMPORT_DATA is not configured");
}
QString clientName = "Test Client (trojan deserialization)";
ImportController::ImportResult importResult;
m_coreController->m_importCoreController->extractConfigFromData(configData);
QString config = configData;
QString prefix;
QString errormsg;
ConfigTypes configType = ConfigTypes::Invalid;
if (config.startsWith("trojan://")) {
configType = ConfigTypes::Xray;
importResult.config = extractXrayConfig(
Utils::JsonToString(serialization::trojan::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact),
configType, prefix);
QVERIFY2(!importResult.config.empty(), "Config shouldn't be empty");
} else {
QSKIP("Config not starts with trojan://");
}
QCOMPARE(importResult.config, config);
}
void testSS()
{
const QString configData = getEnvValue("THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA");
if (!isEnvValueConfigured(configData)) {
QSKIP("THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA is not configured");
}
QString clientName = "Test Client (ss deserialization)";
ImportController::ImportResult importResult;
m_coreController->m_importCoreController->extractConfigFromData(configData);
QString config = configData;
QString prefix;
QString errormsg;
ConfigTypes configType = ConfigTypes::Invalid;
if (config.startsWith("ss://") && !config.contains("plugin=")) {
configType = ConfigTypes::ShadowSocks;
importResult.config = extractXrayConfig(
Utils::JsonToString(serialization::ss::Deserialize(config, &prefix, &errormsg), QJsonDocument::JsonFormat::Compact),
configType, prefix);
QVERIFY2(!importResult.config.empty(), "Config shouldn't be empty");
} else {
QSKIP("Config not starts with ss:// or contain plugin=");
}
QCOMPARE(importResult.config, config);
}
void testSSd()
{
const QString configData = getEnvValue("THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA");
if (!isEnvValueConfigured(configData)) {
QSKIP("THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA is not configured");
}
QString clientName = "Test Client (ssd deserialization)";
ImportController::ImportResult importResult;
m_coreController->m_importCoreController->extractConfigFromData(configData);
QString config = configData;
QString prefix;
QString errormsg;
ConfigTypes configType = ConfigTypes::Invalid;
if (config.startsWith("ssd://")) {
QStringList tmp;
QList<std::pair<QString, QJsonObject>> servers = serialization::ssd::Deserialize(config, &prefix, &tmp);
configType = ConfigTypes::ShadowSocks;
// Took only first config from list
if (!servers.isEmpty()) {
importResult.config = extractXrayConfig(servers.first().first, configType);
}
if (!importResult.config.empty()) {
importResult.configType = configType;
}
QVERIFY2(!importResult.config.empty(), "Config shouldn't be empty");
} else {
QSKIP("Config not starts with ssd://");
}
QCOMPARE(importResult.config, config);
}
};
QTEST_MAIN(TestXraySerialization)
#include "testXraySerialization.moc"
+76
View File
@@ -0,0 +1,76 @@
#ifndef TESTCORECONTROLLER_H
#define TESTCORECONTROLLER_H
#include "core/controllers/coreController.h"
class TestCoreController final : public CoreController
{
public:
explicit TestCoreController(const QSharedPointer<VpnConnection> &vpnConnection,
SecureQSettings *settings,
QQmlApplicationEngine *engine,
QObject *parent = nullptr)
: CoreController(vpnConnection, settings, engine, parent, /*skipPlatformControllerInit=*/true)
, m_serversRepository(serversRepositoryProtected())
, m_appSettingsRepository(appSettingsRepositoryProtected())
, m_serversModel(serversModelProtected())
, m_containersModel(containersModelProtected())
, m_apiServicesModel(apiServicesModelProtected())
, m_newsModel(newsModelProtected())
, m_allowedDnsModel(allowedDnsModelProtected())
, m_appSplitTunnelingModel(appSplitTunnelingModelProtected())
, m_ipSplitTunnelingModel(ipSplitTunnelingModelProtected())
, m_languageModel(languageModelProtected())
, m_connectionUiController(connectionUiControllerProtected())
, m_installUiController(installUiControllerProtected())
, m_importCoreController(importCoreControllerProtected())
, m_exportController(exportControllerProtected())
, m_installController(installControllerProtected())
, m_serversController(serversControllerProtected())
, m_settingsUiController(settingsUiControllerProtected())
, m_settingsController(settingsControllerProtected())
, m_allowedDnsUiController(allowedDnsUiControllerProtected())
, m_allowedDnsController(allowedDnsControllerProtected())
, m_languageUiController(languageUiControllerProtected())
, m_ipSplitTunnelingController(ipSplitTunnelingControllerProtected())
, m_ipSplitTunnelingUiController(ipSplitTunnelingUiControllerProtected())
, m_appSplitTunnelingController(appSplitTunnelingControllerProtected())
, m_appSplitTunnelingUiController(appSplitTunnelingUiControllerProtected())
, m_serversUiController(serversUiControllerProtected())
, m_servicesCatalogUiController(servicesCatalogUiControllerProtected())
, m_apiNewsUiController(apiNewsUiControllerProtected())
{
}
SecureServersRepository *m_serversRepository;
SecureAppSettingsRepository *m_appSettingsRepository;
ServersModel *m_serversModel;
ContainersModel *m_containersModel;
ApiServicesModel *m_apiServicesModel;
NewsModel *m_newsModel;
AllowedDnsModel *m_allowedDnsModel;
AppSplitTunnelingModel *m_appSplitTunnelingModel;
IpSplitTunnelingModel *m_ipSplitTunnelingModel;
LanguageModel *m_languageModel;
ConnectionUiController *m_connectionUiController;
InstallUiController *m_installUiController;
ImportController *m_importCoreController;
ExportController *m_exportController;
InstallController *m_installController;
ServersController *m_serversController;
SettingsUiController *m_settingsUiController;
SettingsController *m_settingsController;
AllowedDnsUiController *m_allowedDnsUiController;
AllowedDnsController *m_allowedDnsController;
LanguageUiController *m_languageUiController;
IpSplitTunnelingController *m_ipSplitTunnelingController;
IpSplitTunnelingUiController *m_ipSplitTunnelingUiController;
AppSplitTunnelingController *m_appSplitTunnelingController;
AppSplitTunnelingUiController *m_appSplitTunnelingUiController;
ServersUiController *m_serversUiController;
ServicesCatalogUiController *m_servicesCatalogUiController;
ApiNewsUiController *m_apiNewsUiController;
};
#endif // TESTCORECONTROLLER_H
+73
View File
@@ -0,0 +1,73 @@
#include "utils/testUtils.h"
#include <QByteArray>
#include <QDebug>
#include "core/repositories/secureServersRepository.h"
#include "core/utils/serverConfigUtils.h"
namespace amnezia::test
{
QString getEnvValue(const QString &key)
{
const QByteArray keyUtf8 = key.toUtf8();
if (qEnvironmentVariableIsSet(keyUtf8.constData())) {
return qEnvironmentVariable(keyUtf8.constData());
}
return QString();
}
void logEnvValueState(const QString &key)
{
const QString value = getEnvValue(key);
qInfo().noquote() << QString("%1: configured=%2 length=%3 value=\"%4\"")
.arg(key,
isEnvValueConfigured(value) ? "yes" : "no",
QString::number(value.size()),
value);
}
bool isEnvValueConfigured(const QString &value)
{
const QString trimmed = value.trimmed();
if (trimmed.isEmpty()) {
return false;
}
if (trimmed.endsWith(QStringLiteral("_config_string"))) {
return false;
}
return true;
}
std::optional<QString> serverDescription(SecureServersRepository *repo, const QString &serverId)
{
switch (repo->serverKind(serverId)) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = repo->selfHostedAdminConfig(serverId);
return cfg ? std::optional<QString>(cfg->description) : std::nullopt;
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = repo->selfHostedUserConfig(serverId);
return cfg ? std::optional<QString>(cfg->description) : std::nullopt;
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = repo->nativeConfig(serverId);
return cfg ? std::optional<QString>(cfg->description) : std::nullopt;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2: {
const auto cfg = repo->apiV2Config(serverId);
return cfg ? std::optional<QString>(cfg->description) : std::nullopt;
}
default:
return std::nullopt;
}
}
std::optional<QString> serverDescriptionAt(SecureServersRepository *repo, int index)
{
return serverDescription(repo, repo->serverIdAt(index));
}
} // namespace amnezia::test
+26
View File
@@ -0,0 +1,26 @@
#ifndef TESTUTILS_H
#define TESTUTILS_H
#include <optional>
#include <QString>
class SecureServersRepository;
namespace amnezia::test
{
QString getEnvValue(const QString &key);
bool isEnvValueConfigured(const QString &value);
// Logs the env var state and value. Secret values coming from GitHub Actions
// secrets are masked as *** in CI logs automatically.
void logEnvValueState(const QString &key);
std::optional<QString> serverDescription(SecureServersRepository *repo, const QString &serverId);
std::optional<QString> serverDescriptionAt(SecureServersRepository *repo, int index);
} // namespace amnezia::test
#endif // TESTUTILS_H
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -31,6 +31,7 @@ int LanguageUiController::getCurrentLanguageIndex() const
case QLocale::Hindi: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Hindi); break;
case QLocale::Korean: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Korean); break;
case QLocale::Spanish: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Spanish); break;
case QLocale::French: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::French); break;
default: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::English); break;
}
}
@@ -65,6 +66,7 @@ LanguageSettings::AvailableLanguageEnum LanguageUiController::getSystemLanguageE
case QLocale::Korean: return LanguageSettings::AvailableLanguageEnum::Korean;
case QLocale::English: return LanguageSettings::AvailableLanguageEnum::English;
case QLocale::Spanish: return LanguageSettings::AvailableLanguageEnum::Spanish;
case QLocale::French: return LanguageSettings::AvailableLanguageEnum::French;
default: return LanguageSettings::AvailableLanguageEnum::English;
}
}
@@ -111,6 +113,7 @@ QString LanguageUiController::getLocalLanguageName(const LanguageSettings::Avail
case LanguageSettings::AvailableLanguageEnum::Hindi: strLanguage = "हिन्दी"; break;
case LanguageSettings::AvailableLanguageEnum::Korean: strLanguage = "한국어"; break;
case LanguageSettings::AvailableLanguageEnum::Spanish: strLanguage = "Español"; break;
case LanguageSettings::AvailableLanguageEnum::French: strLanguage = "Français"; break;
default: break;
}
@@ -131,6 +134,7 @@ QLocale LanguageUiController::languageEnumToLocale(const LanguageSettings::Avail
case LanguageSettings::AvailableLanguageEnum::Hindi: return QLocale::Hindi;
case LanguageSettings::AvailableLanguageEnum::Korean: return QLocale::Korean;
case LanguageSettings::AvailableLanguageEnum::Spanish: return QLocale::Spanish;
case LanguageSettings::AvailableLanguageEnum::French: return QLocale::French;
default: return QLocale::English;
}
}
@@ -12,6 +12,7 @@
#include <utility>
#include "core/utils/api/apiUtils.h"
#include "core/utils/selfhosted/sshExecutor.h"
#include "core/controllers/selfhosted/installController.h"
#include "core/controllers/connectionController.h"
#include "core/utils/networkUtilities.h"
@@ -347,7 +348,7 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
ContainerConfig oldConfigCopy = oldContainerConfig;
InstallController *installController = m_installController;
QFuture<ErrorCode> future =
QtConcurrent::run([installController, serverId, container, oldConfigCopy,
SshExecutor::instance().run(serverId, [installController, serverId, container, oldConfigCopy,
newConfigCopy]() mutable -> ErrorCode {
return installController->updateServerConfig(serverId, container, oldConfigCopy, newConfigCopy);
});
@@ -540,7 +541,7 @@ void InstallUiController::removeContainer(const QString &serverId, int container
});
InstallController *installController = m_installController;
QFuture<ErrorCode> future = QtConcurrent::run(
QFuture<ErrorCode> future = SshExecutor::instance().run(serverId,
[installController, serverId, container]() -> ErrorCode {
return installController->removeContainer(serverId, container);
});
@@ -163,6 +163,10 @@ void SettingsUiController::restoreAppConfigFromData(const QByteArray &data)
emit restoreBackupFinished();
emit autoStartChanged();
emit startMinimizedChanged();
if (m_settingsController->unsupportedFormatConfigsSkippedCount() > 0) {
emit errorOccurred(ErrorCode::RestoreBackupUnsupportedConfigsSkipped);
}
} else {
emit errorOccurred(errorCode);
}
+1
View File
@@ -50,6 +50,7 @@ QString LanguageModel::getLocalLanguageName(const LanguageSettings::AvailableLan
case LanguageSettings::AvailableLanguageEnum::Hindi: strLanguage = "हिन्दी"; break;
case LanguageSettings::AvailableLanguageEnum::Spanish: strLanguage = "Español"; break;
case LanguageSettings::AvailableLanguageEnum::Korean: strLanguage = "한국어"; break;
case LanguageSettings::AvailableLanguageEnum::French: strLanguage = "Français"; break;
default: break;
}
+2 -1
View File
@@ -18,7 +18,8 @@ namespace LanguageSettings
Urdu,
Hindi,
Spanish,
Korean
Korean,
French
};
Q_ENUM_NS(AvailableLanguageEnum)
@@ -196,7 +196,7 @@ PageType {
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 24
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && !(root.currentPlan && root.currentPlan.hasFreeTrial)
visible: Qt.platform.os === "ios" || IsMacOsNeBuild
spacing: 16
ParagraphTextType {
+9
View File
@@ -174,6 +174,15 @@ Window {
PopupType {
id: popupNotificationMessage
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
popupNotificationMessage.close()
popupNotificationTimer.stop()
}
}
}
Timer {
+1
View File
@@ -1,5 +1,6 @@
find_program(CONAN_COMMAND "conan" REQUIRED
HINTS
"${CMAKE_SOURCE_DIR}/.venv/Scripts"
"${CMAKE_SOURCE_DIR}/.venv/bin"
"/opt/homebrew/bin"
)
+7 -7
View File
@@ -1,6 +1,9 @@
if(NOT DEFINED SIGNTOOL_SUBJECT_NAME)
set(SIGNTOOL_SUBJECT_NAME "$ENV{SIGNTOOL_SUBJECT_NAME}")
endif()
if(NOT DEFINED SIGNTOOL_CERT_HAS_UI)
set(SIGNTOOL_CERT_HAS_UI "$ENV{SIGNTOOL_CERT_HAS_UI}")
endif()
if(NOT DEFINED CODESIGN_SIGNATURE)
set(CODESIGN_SIGNATURE "$ENV{CODESIGN_SIGNATURE}")
endif()
@@ -9,14 +12,11 @@ if(NOT DEFINED CODESIGN_KEYCHAIN)
endif()
if(WIN32)
file(GLOB_RECURSE BINARIES
"${CPACK_TEMPORARY_DIRECTORY}/*.dll"
"${CPACK_TEMPORARY_DIRECTORY}/*.exe"
)
if(BINARIES AND SIGNTOOL_SUBJECT_NAME)
set(AMNEZIA_PACKAGE_DIR "${CPACK_TEMPORARY_DIRECTORY}/packages/AmneziaVPN/data")
if(SIGNTOOL_SUBJECT_NAME)
include(${CMAKE_CURRENT_LIST_DIR}/util/signtool.cmake)
signtool_sign_files("${BINARIES}" "${SIGNTOOL_SUBJECT_NAME}")
signtool_sign_files("${AMNEZIA_PACKAGE_DIR}/*.exe" "${SIGNTOOL_SUBJECT_NAME}" "${SIGNTOOL_CERT_HAS_UI}")
signtool_sign_files("${AMNEZIA_PACKAGE_DIR}/*.dll" "${SIGNTOOL_SUBJECT_NAME}" "${SIGNTOOL_CERT_HAS_UI}")
endif()
endif()
+4 -1
View File
@@ -1,6 +1,9 @@
if(NOT DEFINED SIGNTOOL_SUBJECT_NAME)
set(SIGNTOOL_SUBJECT_NAME "$ENV{SIGNTOOL_SUBJECT_NAME}")
endif()
if(NOT DEFINED SIGNTOOL_CERT_HAS_UI)
set(SIGNTOOL_CERT_HAS_UI "$ENV{SIGNTOOL_CERT_HAS_UI}")
endif()
if(NOT DEFINED NOTARYTOOL_EMAIL)
set(NOTARYTOOL_EMAIL "$ENV{NOTARYTOOL_EMAIL}")
endif()
@@ -15,7 +18,7 @@ if(WIN32)
if (SIGNTOOL_SUBJECT_NAME)
include(${CMAKE_CURRENT_LIST_DIR}/util/signtool.cmake)
foreach(PACKAGE_FILE IN LISTS CPACK_PACKAGE_FILES)
signtool_sign_files("${PACKAGE_FILE}" "${SIGNTOOL_SUBJECT_NAME}")
signtool_sign_files("${PACKAGE_FILE}" "${SIGNTOOL_SUBJECT_NAME}" "${SIGNTOOL_CERT_HAS_UI}")
endforeach()
endif()
endif()
+22 -19
View File
@@ -1,10 +1,6 @@
find_program(SIGNTOOL_COMMAND signtool REQUIRED)
function(signtool_sign_files PACKAGE_FILES SUBJECT_NAME)
if(NOT SUBJECT_NAME)
set(SUBJECT_NAME "Privacy Technologies OU")
endif()
function(signtool_sign_files PACKAGE_FILES SUBJECT_NAME CERT_HAS_UI)
if(PACKAGE_FILES)
set(args sign
/n ${SUBJECT_NAME}
@@ -13,26 +9,33 @@ function(signtool_sign_files PACKAGE_FILES SUBJECT_NAME)
/tr http://timestamp.comodoca.com/?td=sha256
)
foreach(file IN LISTS PACKAGE_FILES)
set(local_args ${args})
if (NOT "${file}" MATCHES "\\.msi$")
list(APPEND local_args /as)
endif()
if (NOT "${PACKAGE_FILES}" MATCHES "\\.msi$")
list(APPEND args /as)
endif()
set(cmd ${SIGNTOOL_COMMAND} ${local_args} ${file})
set(cmd ${SIGNTOOL_COMMAND} ${args} ${PACKAGE_FILES})
list(JOIN cmd " " cmd_str)
message(STATUS "${cmd_str}")
list(JOIN cmd " " cmd_str)
message(STATUS "${cmd_str}")
execute_process(
if (CERT_HAS_UI)
execute_process(
COMMAND cmd /c start "" /wait ${cmd}
RESULT_VARIABLE result
)
if(NOT result EQUAL 0)
message(FATAL_ERROR "signtool failed with code ${result}")
endif()
else()
execute_process(
COMMAND ${cmd}
RESULT_VARIABLE result
ERROR_VARIABLE error
)
if(NOT result EQUAL 0)
string(REPLACE "\n" "\n " error " ${error}")
message(FATAL_ERROR "signtool failed:\n${error}")
endif()
endforeach()
if(NOT result EQUAL 0)
string(REPLACE "\n" "\n " error " ${error}")
message(FATAL_ERROR "signtool failed:\n${error}")
endif()
endif()
endif()
endfunction()
+5 -1
View File
@@ -10,6 +10,8 @@ if /i "%~1" == "-i" set "ARG_BUILD_INSTALLERS=!ARG_BUILD_INSTALLERS!
if /i "%~1" == "--installer" set "ARG_BUILD_INSTALLERS=!ARG_BUILD_INSTALLERS! %~2" & shift
if /i "%~1" == "-arch" set "ARCH=%~2" & shift
if /i "%~1" == "--architecture" set "ARCH=%~2" & shift
if /i "%~1" == "--sign-cloud" set "SIGNTOOL_SUBJECT_NAME=%~2" & shift
if /i "%~1" == "--sign-phys" set "SIGNTOOL_SUBJECT_NAME=%~2" & set "SIGNTOOL_CERT_HAS_UI=true" & shift
shift
goto :parse_args
:done_args
@@ -94,8 +96,10 @@ if exist "%VCVARS_PATH%" (
)
:: build project and installers
set "_tests_arg="
if defined AMNEZIA_BUILD_TESTS set "_tests_arg=-DAMNEZIA_BUILD_TESTS=%AMNEZIA_BUILD_TESTS%"
@echo on
cmake -S "%PROJECT_DIR%" -B "%BUILD_DIR%" -DCMAKE_BUILD_TYPE=Release "-DCMAKE_PREFIX_PATH=%QT_ROOT_PATH%\msvc2022_%_qt_postfix_arg%" "-DCMAKE_VS_GLOBALS=UseMultiToolTask=true;EnforceProcessCountAcrossBuilds=true" || goto :fail
cmake -S "%PROJECT_DIR%" -B "%BUILD_DIR%" -DCMAKE_BUILD_TYPE=Release "-DCMAKE_PREFIX_PATH=%QT_ROOT_PATH%\msvc2022_%_qt_postfix_arg%" "-DCMAKE_VS_GLOBALS=UseMultiToolTask=true;EnforceProcessCountAcrossBuilds=true" %_tests_arg% || goto :fail
cmake --build "%BUILD_DIR%" --config Release -- /m || goto :fail
@echo off
for %%I in (%ARG_BUILD_INSTALLERS%) do (
+1
View File
@@ -198,6 +198,7 @@ args=()
[[ -n "$CMAKE_OSX_SYSROOT" ]] && args+=("-DCMAKE_OSX_SYSROOT=$CMAKE_OSX_SYSROOT")
[[ -n "$MACOS_NE" ]] && args+=("-DMACOS_NE=$MACOS_NE")
[[ -n "$DEPLOY" ]] && args+=("-DDEPLOY=$DEPLOY")
[[ -n "$AMNEZIA_BUILD_TESTS" ]] && args+=("-DAMNEZIA_BUILD_TESTS=$AMNEZIA_BUILD_TESTS")
[[ -n "$ANDROID_ABI" ]] && args+=("-DANDROID_ABI=$ANDROID_ABI")
[[ -n "$ANDROID_SDK_ROOT" ]] && args+=("-DANDROID_SDK_ROOT=$ANDROID_SDK_ROOT")
[[ -n "$ANDROID_NDK_ROOT" ]] && args+=("-DANDROID_NDK_ROOT=$ANDROID_NDK_ROOT")
+2 -1
View File
@@ -111,8 +111,9 @@ class AmneziaXrayBindings(ConanFile):
env.define("CGO_LDFLAGS", " ".join(ldflags))
with env.vars(self).apply():
at = Autotools(self)
make_build_dir = build_dir.replace("\\", "/") if self._is_windows else build_dir
at.make(args=[
f"BUILD_DIR={build_dir.replace("\\", "/") if self._is_windows else build_dir}"
f"BUILD_DIR={make_build_dir}"
])
if is_apple_os(self) and self._is_multiarch:
+3 -3
View File
@@ -60,9 +60,9 @@ class HevSocks5Tunnel(ConanFile):
self.run(
f"libtool -static -o {lib_path}"
f" {lib_path}"
f" {os.path.join(self.build_folder, "third-part", "lwip", "bin", "liblwip.a")}"
f" {os.path.join(self.build_folder, "third-part", "yaml", "bin", "libyaml.a")}"
f" {os.path.join(self.build_folder, "third-part", "hev-task-system", "bin", "libhev-task-system.a")}"
f" {os.path.join(self.build_folder, 'third-part', 'lwip', 'bin', 'liblwip.a')}"
f" {os.path.join(self.build_folder, 'third-part', 'yaml', 'bin', 'libyaml.a')}"
f" {os.path.join(self.build_folder, 'third-part', 'hev-task-system', 'bin', 'libhev-task-system.a')}"
)
include_dir = os.path.join(self.build_folder, "framework_include")
+2 -1
View File
@@ -114,8 +114,9 @@ class Tun2Socks(ConanFile):
env.define("CGO_CFLAGS", " ".join(cflags))
with env.vars(self).apply():
at = Autotools(self)
make_build_dir = build_dir.replace("\\", "/") if self._is_windows else build_dir
at.make("tun2socks", args=[
f"BUILD_DIR={build_dir.replace("\\", "/") if self._is_windows else build_dir}"
f"BUILD_DIR={make_build_dir}"
])
if self._is_windows:
os.rename(