From 9b947ffb91a8c949fe47c517c85b60e8286c94c5 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 3 Jan 2024 19:01:40 +0100 Subject: [PATCH] Use baasaas on CI (#1441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use baasaas on CI * Remove dependency on realm in the baas client * Regenerate models * Cleanup containers after test runs * Fix CI * Try setting the baas url correctly * Fix order of test setup * .. * Add some missing flutter tests * Try to fix flutter tests * Rework indexed test to use values from the second half of the table. * Use api key auth * Post-merge fixes * Use endpoints, wire-up differentiator tag * Guard against incomplete containers * Fix cleanup workflow; fix env variable names * Don't log isolate warnings when the isolate is created by dart test * Pass correct differentiator to android tests * Always run cleanup * Rework cleanup command * Use reporter * Don't fail test run on test errors * Fail on error * Remove failing test * Update lib/src/cli/atlas_apps/options.dart Co-authored-by: Kasper Overgård Nielsen * Support maps (#1406) * Update generator to support maps * Generator updates * Better error message for wrong key type * Remove the default value lists * Add tests for non-empty default collection initializers * Wire up some of the implementation * Wire up most of the test infrastructure * Add notification tests * Fix generator test expects * Fix tests * Add changelog, clean up the generator a little * Revert some unneeded changes * Fix expectations --------- Co-authored-by: Nikola Irinchev * Merge main, regenerate cli --------- Co-authored-by: Kasper Overgård Nielsen --- .github/workflows/ci.yml | 539 +++++++----------- .github/workflows/cleanup-clusters.yml | 17 - .github/workflows/create-cluster.yml | 49 -- .github/workflows/dart-desktop-tests.yml | 40 +- .github/workflows/deploy-baas.yml | 35 ++ .github/workflows/flutter-desktop-tests.yml | 29 +- .github/workflows/shared-apps.yml | 58 -- .github/workflows/terminate-baas.yml | 35 ++ .../tests/test_driver/app_test.dart | 3 +- .../tests/test_driver/realm_test.dart | 8 +- lib/src/app.dart | 6 +- lib/src/cli/atlas_apps/baas_client.dart | 252 +++++--- .../cli/atlas_apps/deleteapps_command.dart | 17 +- .../cli/atlas_apps/deployapps_command.dart | 39 +- lib/src/cli/atlas_apps/options.dart | 9 +- lib/src/cli/atlas_apps/options.g.dart | 9 +- test/baas_helper.dart | 257 +++++++++ test/client_reset_test.dart | 82 +-- test/configuration_test.dart | 6 +- test/indexed_test.dart | 25 +- test/list_test.dart | 1 + test/test.dart | 199 +------ test/user_test.dart | 4 +- 23 files changed, 866 insertions(+), 853 deletions(-) delete mode 100644 .github/workflows/cleanup-clusters.yml delete mode 100644 .github/workflows/create-cluster.yml create mode 100644 .github/workflows/deploy-baas.yml delete mode 100644 .github/workflows/shared-apps.yml create mode 100644 .github/workflows/terminate-baas.yml create mode 100644 test/baas_helper.dart diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b31de514b..2d3d96e5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,6 @@ on: - main pull_request: env: - BAAS_URL: ${{ secrets.REALM_QA_URL }} - BAAS_API_KEY: ${{ secrets.ATLAS_QA_PUBLIC_API_KEY }} - BAAS_PRIVATE_API_KEY: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - BAAS_PROJECT_ID: ${{ secrets.ATLAS_QA_PROJECT_ID}} REALM_CI: true concurrency: @@ -17,224 +13,6 @@ concurrency: cancel-in-progress: true jobs: - deploy-dart-cluster: - name: Deploy Dart cluster - secrets: inherit - uses: ./.github/workflows/create-cluster.yml - with: - prefix: d - - deploy-flutter-cluster: - name: Deploy Flutter cluster - secrets: inherit - uses: ./.github/workflows/create-cluster.yml - with: - prefix: f - - create-dart-shared-apps: - name: Create Dart shared Apps - secrets: inherit - needs: - - deploy-dart-cluster - uses: ./.github/workflows/shared-apps.yml - with: - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} - env: Dart - - create-flutter-shared-apps: - name: Create Flutter shared Apps - secrets: inherit - needs: - - deploy-flutter-cluster - uses: ./.github/workflows/shared-apps.yml - with: - cluster: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} - env: Flutter - - delete-dart-cluster: - runs-on: ubuntu-latest - name: Delete Dart Cluster - timeout-minutes: 5 - continue-on-error: true - needs: - - deploy-dart-cluster - - dart-tests-windows - - dart-tests-macos - - dart-tests-macos-arm - - dart-tests-linux - - dart-tests-linux-ubuntu-20 - - steps: - - uses: realm/ci-actions/mdb-realm/cleanup@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ needs.deploy-dart-cluster.outputs.clusterName }} - - delete-flutter-cluster: - runs-on: ubuntu-latest - name: Delete Flutter Cluster - timeout-minutes: 5 - continue-on-error: true - needs: - - deploy-flutter-cluster - - flutter-desktop-tests-windows - - flutter-desktop-tests-macos - - flutter-desktop-tests-linux - - flutter-desktop-tests-linux-ubuntu-20 - - flutter-ios - - flutter-android - steps: - - uses: realm/ci-actions/mdb-realm/cleanup@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} - - cleanup-dart-shared-apps: - name: Delete Dart shared Apps - secrets: inherit - needs: - - deploy-dart-cluster - - dart-tests-windows - - dart-tests-macos - - dart-tests-macos-arm - - dart-tests-linux - - dart-tests-linux-ubuntu-20 - uses: ./.github/workflows/shared-apps.yml - if: always() - with: - cleanup: true - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} - env: Dart - - cleanup-flutter-shared-apps: - name: Delete Flutter shared Apps - secrets: inherit - needs: - - deploy-flutter-cluster - - flutter-desktop-tests-windows - - flutter-desktop-tests-macos - - flutter-desktop-tests-linux - - flutter-desktop-tests-linux-ubuntu-20 - - flutter-ios - - flutter-android - uses: ./.github/workflows/shared-apps.yml - if: always() - with: - cleanup: true - cluster: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} - env: Flutter - - cleanup-dart-matrix: - needs: - - deploy-dart-cluster - - dart-tests-windows - - dart-tests-macos - - dart-tests-macos-arm - - dart-tests-linux - - dart-tests-linux-ubuntu-20 - strategy: - fail-fast: false - matrix: - include: - - app: dm - description: dart macos - - app: dma - description: dart macos-arm - - app: dl - description: dart linux - - app: dl2 - description: dart linux (ubuntu 20) - - app: dw - description: dart windows - runs-on: ubuntu-latest - name: Cleanup apps for ${{ matrix.description }} - timeout-minutes: 20 - if: always() - env: - BAAS_CLUSTER: ${{ needs.deploy-dart-cluster.outputs.clusterName }} - BAAS_DIFFERENTIATOR: ${{ matrix.app }}${{ github.run_id }}${{ github.run_attempt }} - steps: - - name: Checkout - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - with: - submodules: false - - - name : Setup Dart SDK - uses: dart-lang/setup-dart@main - with: - sdk: stable - - - name: Cleanup Dart apps - run: | - dart run realm_dart delete-apps \ - --baas-url ${{ env.BAAS_URL }} \ - --atlas-cluster ${{ env.BAAS_CLUSTER }} \ - --api-key ${{ env.BAAS_API_KEY }} \ - --private-api-key ${{ env.BAAS_PRIVATE_API_KEY }} \ - --project-id ${{ env.BAAS_PROJECT_ID }} \ - --differentiator '${{ env.BAAS_DIFFERENTIATOR }}' - - cleanup-flutter-matrix: - needs: - - deploy-flutter-cluster - - flutter-desktop-tests-windows - - flutter-desktop-tests-macos - - flutter-desktop-tests-linux - - flutter-desktop-tests-linux-ubuntu-20 - - flutter-ios - - flutter-android - strategy: - fail-fast: false - matrix: - include: - - app: fm - description: flutter macos - - app: fl - description: flutter linux - - app: fl2 - description: flutter linux (ubuntu 20) - - app: fw - description: flutter windows - - app: fa - description: flutter android - - app: fi - description: flutter iOS - runs-on: ubuntu-latest - name: Cleanup apps for ${{ matrix.description }} - timeout-minutes: 20 - if: always() - env: - BAAS_CLUSTER: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} - BAAS_DIFFERENTIATOR: ${{ matrix.app }}${{ github.run_id }}${{ github.run_attempt }} - steps: - - name: Checkout - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - with: - submodules: false - - - name : Setup Dart SDK - uses: dart-lang/setup-dart@main - with: - sdk: stable - - - name: Cleanup Flutter apps - run: | - dart run realm_dart delete-apps \ - --baas-url ${{ env.BAAS_URL }} \ - --atlas-cluster ${{ env.BAAS_CLUSTER }} \ - --api-key ${{ env.BAAS_API_KEY }} \ - --private-api-key ${{ env.BAAS_PRIVATE_API_KEY }} \ - --project-id ${{ env.BAAS_PROJECT_ID }} \ - --differentiator '${{ env.BAAS_DIFFERENTIATOR }}' - build-windows: name: Build Windows uses: ./.github/workflows/build-native.yml @@ -276,160 +54,242 @@ jobs: build: '["ios-device", "ios-simulator", "ios-catalyst"]' build-android-combined: - name: Android binaries combine + name: Build combine Android needs: build-android uses: ./.github/workflows/binary-combine-android.yml build-ios-xcframework: - name: IOS binaries combine + name: Build combine iOS needs: build-ios uses: ./.github/workflows/binary-combine-ios.yml # Dart jobs + deploy-cluster-dart-windows: + name: Deploy Cluster for Dart Windows + uses: ./.github/workflows/deploy-baas.yml + secrets: inherit + with: + differentiator: dw${{ github.run_id }}${{ github.run_attempt }} + dart-tests-windows: - name: Windows Dart Tests + name: Dart Tests Windows uses: ./.github/workflows/dart-desktop-tests.yml needs: - build-windows - - deploy-dart-cluster - - create-dart-shared-apps + - deploy-cluster-dart-windows secrets: inherit with: - os: windows - runner: windows-latest - app: dw - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} + os: windows + runner: windows-latest + differentiator: dw${{ github.run_id }}${{ github.run_attempt }} + + cleanup-cluster-dart-windows: + name: Cleanup Cluster for Dart Windows + uses: ./.github/workflows/terminate-baas.yml + if: always() + needs: + - dart-tests-windows + secrets: inherit + with: + differentiator: dw${{ github.run_id }}${{ github.run_attempt }} + + deploy-cluster-dart-macos: + name: Deploy Cluster for Dart MacOS + uses: ./.github/workflows/deploy-baas.yml + secrets: inherit + with: + differentiator: dm${{ github.run_id }}${{ github.run_attempt }} dart-tests-macos: - name: MacOS Dart Tests + name: Dart Tests MacOS uses: ./.github/workflows/dart-desktop-tests.yml needs: - build-macos - - deploy-dart-cluster - - create-dart-shared-apps + - deploy-cluster-dart-macos secrets: inherit with: - os: macos - runner: macos-latest - app: dm - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} + os: macos + runner: macos-latest + differentiator: dm${{ github.run_id }}${{ github.run_attempt }} + + cleanup-cluster-dart-macos: + name: Cleanup Cluster for Dart macOS + uses: ./.github/workflows/terminate-baas.yml + if: always() + needs: + - dart-tests-macos + secrets: inherit + with: + differentiator: dm${{ github.run_id }}${{ github.run_attempt }} + + deploy-cluster-dart-macos-arm: + name: Deploy Cluster for Dart MacOS Arm + uses: ./.github/workflows/deploy-baas.yml + secrets: inherit + with: + differentiator: dma${{ github.run_id }}${{ github.run_attempt }} dart-tests-macos-arm: - name: MacOS Arm Dart Tests + name: Dart Tests MacOS Arm uses: ./.github/workflows/dart-desktop-tests.yml needs: - build-macos - - deploy-dart-cluster - - create-dart-shared-apps + - deploy-cluster-dart-macos-arm secrets: inherit with: - os: macos - runner: macos-arm - architecture: arm - app: dma - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} + os: macos + runner: macos-arm + architecture: arm + differentiator: dma${{ github.run_id }}${{ github.run_attempt }} + + cleanup-cluster-dart-macos-arm: + name: Cleanup Cluster for Dart macOS Arm + uses: ./.github/workflows/terminate-baas.yml + if: always() + needs: + - dart-tests-macos-arm + secrets: inherit + with: + differentiator: dma${{ github.run_id }}${{ github.run_attempt }} + + deploy-cluster-dart-linux: + name: Deploy Cluster for Dart Linux + uses: ./.github/workflows/deploy-baas.yml + secrets: inherit + with: + differentiator: dl${{ github.run_id }}${{ github.run_attempt }} dart-tests-linux: - name: Linux Dart Tests + name: Dart Tests Linux uses: ./.github/workflows/dart-desktop-tests.yml needs: - build-linux - - deploy-dart-cluster - - create-dart-shared-apps + - deploy-cluster-dart-linux secrets: inherit with: - os: linux - runner: ubuntu-latest - app: dl - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} + os: linux + runner: ubuntu-latest + differentiator: dl${{ github.run_id }}${{ github.run_attempt }} - dart-tests-linux-ubuntu-20: - name: Linux Dart Tests (ubuntu 20) - uses: ./.github/workflows/dart-desktop-tests.yml + cleanup-cluster-dart-linux: + name: Cleanup Cluster for Dart Linux + uses: ./.github/workflows/terminate-baas.yml + if: always() needs: - - build-linux - - deploy-dart-cluster - - create-dart-shared-apps + - dart-tests-linux secrets: inherit with: - os: linux - runner: ubuntu-20.04 - app: dl2 - cluster: ${{ needs.deploy-dart-cluster.outputs.clusterName }} + differentiator: dl${{ github.run_id }}${{ github.run_attempt }} # Flutter jobs + deploy-cluster-flutter-windows: + name: Deploy Cluster for Flutter Windows + uses: ./.github/workflows/deploy-baas.yml + secrets: inherit + with: + differentiator: fw${{ github.run_id }}${{ github.run_attempt }} - flutter-desktop-tests-windows: - name: Windows Flutter Tests + flutter-tests-windows: + name: Flutter Tests Windows uses: ./.github/workflows/flutter-desktop-tests.yml needs: - build-windows - - deploy-flutter-cluster - - create-flutter-shared-apps + - deploy-cluster-flutter-windows secrets: inherit with: - os: windows - runner: windows-latest - app: fw - cluster: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} + os: windows + runner: windows-latest + differentiator: fw${{ github.run_id }}${{ github.run_attempt }} + + + cleanup-cluster-flutter-windows: + name: Cleanup Cluster for Flutter Windows + uses: ./.github/workflows/terminate-baas.yml + if: always() + needs: + - flutter-tests-windows + secrets: inherit + with: + differentiator: fw${{ github.run_id }}${{ github.run_attempt }} + + deploy-cluster-flutter-macos: + name: Deploy Cluster for Flutter MacOS + uses: ./.github/workflows/deploy-baas.yml + secrets: inherit + with: + differentiator: fm${{ github.run_id }}${{ github.run_attempt }} - flutter-desktop-tests-macos: - name: MacOS Flutter Tests + flutter-tests-macos: + name: Flutter Tests MacOS uses: ./.github/workflows/flutter-desktop-tests.yml needs: - build-macos - - deploy-flutter-cluster - - create-flutter-shared-apps + - deploy-cluster-flutter-macos secrets: inherit with: - os: macos - runner: macos-13 # workaround to: https://github.com/flutter/flutter/issues/118469 latest is still macos-12 ¯\_(ツ)_/¯ - app: fm - cluster: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} + os: macos + runner: macos-13 # workaround to: https://github.com/flutter/flutter/issues/118469 latest is still macos-12 ¯\_(ツ)_/¯ + differentiator: fm${{ github.run_id }}${{ github.run_attempt }} - flutter-desktop-tests-linux: - name: Linux Flutter Tests - uses: ./.github/workflows/flutter-desktop-tests.yml + cleanup-cluster-flutter-macos: + name: Cleanup Cluster for Flutter macOS + uses: ./.github/workflows/terminate-baas.yml + if: always() needs: - - build-linux - - deploy-flutter-cluster - - create-flutter-shared-apps + - flutter-tests-macos secrets: inherit with: - os: linux - runner: ubuntu-latest - app: fl - cluster: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} + differentiator: fm${{ github.run_id }}${{ github.run_attempt }} - flutter-desktop-tests-linux-ubuntu-20: - name: Linux Flutter Tests (ubuntu 20) + deploy-cluster-flutter-linux: + name: Deploy Cluster for Flutter Linux + uses: ./.github/workflows/deploy-baas.yml + secrets: inherit + with: + differentiator: fl${{ github.run_id }}${{ github.run_attempt }} + + flutter-tests-linux: + name: Flutter Tests Linux uses: ./.github/workflows/flutter-desktop-tests.yml needs: - build-linux - - deploy-flutter-cluster - - create-flutter-shared-apps + - deploy-cluster-flutter-linux secrets: inherit with: - os: linux - runner: ubuntu-20.04 - app: fl2 - cluster: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} + os: linux + runner: ubuntu-latest + differentiator: fl${{ github.run_id }}${{ github.run_attempt }} - flutter-ios: + cleanup-cluster-flutter-linux: + name: Cleanup Cluster for Flutter Linux + uses: ./.github/workflows/terminate-baas.yml + if: always() + needs: + - flutter-tests-linux + secrets: inherit + with: + differentiator: fl${{ github.run_id }}${{ github.run_attempt }} + + deploy-cluster-flutter-ios: + name: Deploy Cluster for Flutter iOS + uses: ./.github/workflows/deploy-baas.yml + secrets: inherit + with: + differentiator: fi${{ github.run_id }}${{ github.run_attempt }} + + flutter-tests-ios: runs-on: macos-latest - name: IOS Flutter Tests + name: Flutter Tests iOS timeout-minutes: 45 needs: - - deploy-flutter-cluster + - deploy-cluster-flutter-ios - build-ios-xcframework - - create-flutter-shared-apps env: - BAAS_CLUSTER: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} BAAS_DIFFERENTIATOR: fi${{ github.run_id }}${{ github.run_attempt }} + BAAS_BAASAAS_API_KEY: ${{ secrets.BAASAAS_API_KEY}} steps: - - name: Checkout uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 with: @@ -459,36 +319,40 @@ jobs: os: 'iOS' os_version: '>= 14.0' - # This will be a no-op under normal circumstances since the cluster would have been deployed - # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - - name: Create cluster - uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ env.BAAS_CLUSTER }} - - name: Run tests on iOS Simulator run: | flutter drive --target=test_driver/app.dart --dart-define=testName="" --suppress-analytics --debug working-directory: ./flutter/realm_flutter/tests - flutter-android: + cleanup-cluster-flutter-ios: + name: Cleanup Cluster for Flutter iOS + uses: ./.github/workflows/terminate-baas.yml + if: always() + needs: + - flutter-tests-ios + secrets: inherit + with: + differentiator: fi${{ github.run_id }}${{ github.run_attempt }} + + deploy-cluster-flutter-android: + name: Deploy Cluster for Flutter Android + uses: ./.github/workflows/deploy-baas.yml + secrets: inherit + with: + differentiator: fa${{ github.run_id }}${{ github.run_attempt }} + + flutter-tests-android: runs-on: macos-latest - name: Android Flutter Tests + name: Flutter Tests Android timeout-minutes: 45 needs: - - deploy-flutter-cluster + - deploy-cluster-flutter-android - build-android-combined - - create-flutter-shared-apps env: - BAAS_CLUSTER: ${{ needs.deploy-flutter-cluster.outputs.clusterName }} BAAS_DIFFERENTIATOR: fa${{ github.run_id }}${{ github.run_attempt }} - steps: + BAAS_BAASAAS_API_KEY: ${{ secrets.BAASAAS_API_KEY}} + steps: - name: Checkout uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 with: @@ -537,18 +401,6 @@ jobs: cmake: 3.10.2.4988404 script: echo "Generated Emulator snapshot for caching." - # This will be a no-op under normal circumstances since the cluster would have been deployed - # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - - name: Create cluster - uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ env.BAAS_CLUSTER }} - - name: Run tests on Android Emulator uses: reactivecircus/android-emulator-runner@v2 with: @@ -562,6 +414,16 @@ jobs: script: flutter build apk --debug --target=test_driver/app.dart && flutter install --debug && flutter drive --target=test_driver/app.dart --dart-define=testName="" --suppress-analytics --debug working-directory: ./flutter/realm_flutter/tests + cleanup-cluster-flutter-android: + name: Cleanup Cluster for Flutter Android + uses: ./.github/workflows/terminate-baas.yml + if: always() + needs: + - flutter-tests-android + secrets: inherit + with: + differentiator: fa${{ github.run_id }}${{ github.run_attempt }} + # Generator jobs generator: @@ -674,8 +536,15 @@ jobs: slack-on-failure: name: Report failure in main branch needs: - - cleanup-dart-matrix - - cleanup-flutter-matrix + - dart-tests-linux + - dart-tests-macos + - dart-tests-macos-arm + - dart-tests-windows + - flutter-tests-linux + - flutter-tests-macos + - flutter-tests-windows + - flutter-tests-ios + - flutter-tests-android runs-on: ubuntu-latest if: always() && github.ref == 'refs/heads/main' steps: diff --git a/.github/workflows/cleanup-clusters.yml b/.github/workflows/cleanup-clusters.yml deleted file mode 100644 index 8c0ceb244..000000000 --- a/.github/workflows/cleanup-clusters.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Wipe all clusters and apps - -on: - workflow_dispatch: -jobs: - main: - runs-on: ubuntu-latest - name: Wipe all clusters and apps - steps: - - uses: realm/ci-actions/mdb-realm/deleteAllClusters@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ secrets.REALM_QA_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ secrets.ATLAS_QA_PROJECT_ID }} - apiKey: ${{ secrets.ATLAS_QA_PUBLIC_API_KEY }} - privateApiKey: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - diff --git a/.github/workflows/create-cluster.yml b/.github/workflows/create-cluster.yml deleted file mode 100644 index 8dc89c89d..000000000 --- a/.github/workflows/create-cluster.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Create cluster - -on: - workflow_call: - inputs: - prefix: - description: Cluster name prefix. - required: true - type: string - outputs: - clusterName: - description: "The name of created Cluster" - value: ${{ jobs.deploy-cluster.outputs.clusterName }} - -env: - BAAS_URL: ${{ secrets.REALM_QA_URL }} - BAAS_API_KEY: ${{ secrets.ATLAS_QA_PUBLIC_API_KEY }} - BAAS_PRIVATE_API_KEY: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - BAAS_PROJECT_ID: ${{ secrets.ATLAS_QA_PROJECT_ID}} - REALM_CI: true - -jobs: - deploy-cluster: - runs-on: ubuntu-latest - name: Deploy ${{ inputs.prefix == 'd' && 'Dart' || 'Flutter' }} Cluster - timeout-minutes: 15 - outputs: - clusterName: ${{ steps.cluster-name.outputs.clusterName }} - steps: - - name: Get cluster suffix - id: cluster-name - # Use 'github.ref_name' for generating cluster name. - # 'github.ref_name' is the SHORT ref name of the branch or tag that triggered the workflow run. - # 'github.ref_name' looks like '1234/merge'. We remove '/merge' and get only the number. - # In order to have unique cluster name per run we add a random number to the number extracted from 'ref_name'. - # Maximum 8 symbols could be taken for the name of the cluster, becasuse of the lenght limitation for cluster names. - run: | - triggerName=${{ inputs.prefix }}${{ github.ref_name}} - cluster=${triggerName/'/merge'/''} - echo "clusterName=$(cut -c 1-8 <<< ${cluster}$RANDOM)" >> $GITHUB_OUTPUT - - - uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ steps.cluster-name.outputs.clusterName }} \ No newline at end of file diff --git a/.github/workflows/dart-desktop-tests.yml b/.github/workflows/dart-desktop-tests.yml index c8b9df310..c5a157a3f 100644 --- a/.github/workflows/dart-desktop-tests.yml +++ b/.github/workflows/dart-desktop-tests.yml @@ -15,30 +15,21 @@ on: description: Architecture to execute on. required: false type: string - app: - description: App name prefix. - required: true - type: string - cluster: - description: Cluster name to deploy. + differentiator: + description: Differentiator for the BaaS container. required: true type: string env: - BAAS_URL: ${{ secrets.REALM_QA_URL }} - BAAS_API_KEY: ${{ secrets.ATLAS_QA_PUBLIC_API_KEY }} - BAAS_PRIVATE_API_KEY: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - BAAS_PROJECT_ID: ${{ secrets.ATLAS_QA_PROJECT_ID}} REALM_CI: true + BAAS_BAASAAS_API_KEY: ${{ secrets.BAASAAS_API_KEY}} + BAAS_DIFFERENTIATOR: ${{ inputs.differentiator }} jobs: dart-tests: runs-on: ${{ inputs.runner }} name: Dart tests on ${{inputs.os }} ${{ inputs.architecture }} timeout-minutes: 45 - env: - BAAS_CLUSTER: ${{ inputs.cluster }} - BAAS_DIFFERENTIATOR: ${{ inputs.app }}${{ github.run_id }}${{ github.run_attempt }} steps: - name: Checkout @@ -68,20 +59,17 @@ jobs: run: ulimit -n 10240 if: ${{ contains(inputs.os, 'macos') }} - # This will be a no-op under normal circumstances since the cluster would have been deployed - # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - - name: Create cluster - uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ env.BAAS_CLUSTER }} - - name: Run tests - run: ${{ inputs.architecture == 'arm' && 'arch -arm64 ' || '' }}dart test -r expanded --coverage ./coverage/ -j 1 --test-randomize-ordering-seed random + run: ${{ inputs.architecture == 'arm' && 'arch -arm64 ' || '' }}dart test -r expanded --coverage ./coverage/ -j 1 --test-randomize-ordering-seed random --file-reporter="json:test-results.json" || true + + - name: Publish Test Report + uses: dorny/test-reporter@v1.7.0 + if: success() || failure() + with: + name: Test Results Dart ${{ inputs.os }} ${{ inputs.architecture }} + path: test-results.json + reporter: dart-json + only-summary: true # we're pruning generated files, the cli folder, as well as realm_bindings.dart from our coverage reports - name: Generate realm_dart coverage report diff --git a/.github/workflows/deploy-baas.yml b/.github/workflows/deploy-baas.yml new file mode 100644 index 000000000..7dcc06302 --- /dev/null +++ b/.github/workflows/deploy-baas.yml @@ -0,0 +1,35 @@ +name: Deploy BaaS and apps + +on: + workflow_call: + inputs: + differentiator: + description: Differentiator for the BaaS container. + required: true + type: string + +env: + REALM_CI: true + +jobs: + deploy-baas: + runs-on: ubuntu-latest + name: Deploy BaaS + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + submodules: false + + - name : Setup Dart SDK + uses: dart-lang/setup-dart@main + with: + sdk: stable + architecture: 'x64' + + - name: Install dependencies + run: dart pub get + + - name: Deploy cluster and apps + run: dart run realm_dart deploy-apps --baasaas-api-key ${{ secrets.BAASAAS_API_KEY }} --differentiator ${{ inputs.differentiator }} diff --git a/.github/workflows/flutter-desktop-tests.yml b/.github/workflows/flutter-desktop-tests.yml index 8f7568f60..c808d1e59 100644 --- a/.github/workflows/flutter-desktop-tests.yml +++ b/.github/workflows/flutter-desktop-tests.yml @@ -15,20 +15,12 @@ on: description: Architecture to execute on. required: false type: string - app: - description: App name prefix - required: true - type: string - cluster: - description: Cluster name to deploy. + differentiator: + description: Differentiator for the BaaS container. required: true type: string env: - BAAS_URL: ${{ secrets.REALM_QA_URL }} - BAAS_API_KEY: ${{ secrets.ATLAS_QA_PUBLIC_API_KEY }} - BAAS_PRIVATE_API_KEY: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - BAAS_PROJECT_ID: ${{ secrets.ATLAS_QA_PROJECT_ID}} REALM_CI: true jobs: @@ -37,8 +29,9 @@ jobs: name: Flutter tests on ${{inputs.os }}-${{ inputs.architecture }} timeout-minutes: 45 env: - BAAS_CLUSTER: ${{ inputs.cluster }} - BAAS_DIFFERENTIATOR: ${{ inputs.app }}${{ github.run_id }}${{ github.run_attempt }} + BAAS_BAASAAS_API_KEY: ${{ secrets.BAASAAS_API_KEY}} + BAAS_DIFFERENTIATOR: ${{ inputs.differentiator }} + steps: - name: Checkout @@ -78,18 +71,6 @@ jobs: run: ulimit -n 10240 if: ${{ contains(inputs.os, 'macos') }} - # This will be a no-op under normal circumstances since the cluster would have been deployed - # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - - name: Create cluster - uses: realm/ci-actions/mdb-realm/deploy@338bf3e7575015a28faec8b67614385d122aece7 - with: - realmUrl: ${{ env.BAAS_URL }} - atlasUrl: ${{ secrets.ATLAS_QA_URL }} - projectId: ${{ env.BAAS_PROJECT_ID }} - apiKey: ${{ env.BAAS_API_KEY }} - privateApiKey: ${{ env.BAAS_PRIVATE_API_KEY }} - clusterName: ${{ env.BAAS_CLUSTER }} - - name: Run tests run: ${{ inputs.os == 'linux' && 'xvfb-run' || '' }} flutter drive -d ${{ inputs.os }} --target=test_driver/app.dart --suppress-analytics --dart-entrypoint-args="" --debug # -a="Some test name" working-directory: ./flutter/realm_flutter/tests diff --git a/.github/workflows/shared-apps.yml b/.github/workflows/shared-apps.yml deleted file mode 100644 index 74d213cba..000000000 --- a/.github/workflows/shared-apps.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Shared Apps - -on: - workflow_call: - inputs: - env: - description: Dart or Flutter. - required: true - type: string - cluster: - description: Cluster name to deploy the apps. - required: true - type: string - cleanup: - description: Set to True to delete the shared apps. - required: false - default: false - type: boolean - -env: - BAAS_URL: ${{ secrets.REALM_QA_URL }} - BAAS_API_KEY: ${{ secrets.ATLAS_QA_PUBLIC_API_KEY }} - BAAS_PRIVATE_API_KEY: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - BAAS_PROJECT_ID: ${{ secrets.ATLAS_QA_PROJECT_ID}} - REALM_CI: true - -jobs: - shared-apps: - runs-on: ubuntu-latest - name: ${{ inputs.cleanup && 'Delete' || 'Create'}} ${{ inputs.env }} Shared Apps - timeout-minutes: 45 - env: - BAAS_CLUSTER: ${{ inputs.cluster }} - - steps: - - name: Checkout - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - with: - submodules: false - - - name : Setup Dart SDK - uses: dart-lang/setup-dart@main - with: - sdk: stable - architecture: 'x64' - - - name: Install dependencies - run: dart pub get - - - name: ${{ inputs.cleanup && 'Delete' || 'Create'}} shared apps - run: | - dart run realm_dart ${{ inputs.cleanup && 'delete-apps' || 'deploy-apps'}} \ - --baas-url ${{ env.BAAS_URL }} \ - --atlas-cluster ${{ env.BAAS_CLUSTER }} \ - --api-key ${{ env.BAAS_API_KEY }} \ - --private-api-key ${{ env.BAAS_PRIVATE_API_KEY }} \ - --project-id ${{ env.BAAS_PROJECT_ID }} \ - --differentiator 'shared' diff --git a/.github/workflows/terminate-baas.yml b/.github/workflows/terminate-baas.yml new file mode 100644 index 000000000..cf1de2da5 --- /dev/null +++ b/.github/workflows/terminate-baas.yml @@ -0,0 +1,35 @@ +name: Terminate BaaS + +on: + workflow_call: + inputs: + differentiator: + description: Differentiator for the BaaS container. + required: true + type: string + +env: + REALM_CI: true + +jobs: + terminate-baas: + runs-on: ubuntu-latest + name: Terminate BaaS + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + submodules: false + + - name : Setup Dart SDK + uses: dart-lang/setup-dart@main + with: + sdk: stable + architecture: 'x64' + + - name: Install dependencies + run: dart pub get + + - name: Terminate baas + run: dart run realm_dart delete-apps --baasaas-api-key ${{ secrets.BAASAAS_API_KEY }} --differentiator ${{ inputs.differentiator }} diff --git a/flutter/realm_flutter/tests/test_driver/app_test.dart b/flutter/realm_flutter/tests/test_driver/app_test.dart index ad9b89170..1f914a9c8 100644 --- a/flutter/realm_flutter/tests/test_driver/app_test.dart +++ b/flutter/realm_flutter/tests/test_driver/app_test.dart @@ -6,8 +6,6 @@ import 'package:test/test.dart'; import 'const.dart'; void main(List args) { - print("Current PID $pid"); - group('Realm tests', () { FlutterDriver? driver; @@ -28,6 +26,7 @@ void main(List args) { testCommandWithArgs += getArgFromEnvVariable("BAAS_PRIVATE_API_KEY"); testCommandWithArgs += getArgFromEnvVariable("BAAS_PROJECT_ID"); testCommandWithArgs += getArgFromEnvVariable("BAAS_DIFFERENTIATOR"); + testCommandWithArgs += getArgFromEnvVariable("BAAS_BAASAAS_API_KEY"); String result = await driver!.requestData(testCommandWithArgs, timeout: const Duration(minutes: 30)); if (result.isNotEmpty) { diff --git a/flutter/realm_flutter/tests/test_driver/realm_test.dart b/flutter/realm_flutter/tests/test_driver/realm_test.dart index 758e40d54..3373d62cc 100644 --- a/flutter/realm_flutter/tests/test_driver/realm_test.dart +++ b/flutter/realm_flutter/tests/test_driver/realm_test.dart @@ -7,6 +7,7 @@ import 'package:test_api/src/backend/invoker.dart'; import 'package:test_api/src/backend/state.dart' as test_api; import '../test/app_test.dart' as app_test; +import '../test/asymmetric_test.dart' as asymmetric_test; import '../test/backlinks_test.dart' as backlinks_test; import '../test/client_reset_test.dart' as client_reset_test; import '../test/configuration_test.dart' as configuration_test; @@ -14,6 +15,7 @@ import '../test/credentials_test.dart' as credentials_test; import '../test/decimal128_test.dart' as decimal128_test; import '../test/dynamic_realm_test.dart' as dynamic_realm_test; import '../test/embedded_test.dart' as embedded_test; +import '../test/geospatial_test.dart' as geospatial_test; import '../test/indexed_test.dart' as indexed_test; import '../test/list_test.dart' as list_test; import '../test/migration_test.dart' as migration_test; @@ -34,6 +36,7 @@ Future main(List args) async { final List failedTests = []; await app_test.main(args); + await asymmetric_test.main(args); await backlinks_test.main(args); await client_reset_test.main(args); await configuration_test.main(args); @@ -41,9 +44,11 @@ Future main(List args) async { await decimal128_test.main(args); await dynamic_realm_test.main(args); await embedded_test.main(args); - indexed_test.main(args); + await geospatial_test.main(args); + await indexed_test.main(args); await list_test.main(args); await migration_test.main(args); + await realm_logger_test.main(args); await realm_object_test.main(args); await realm_set_test.main(args); await realm_test.main(args); @@ -52,7 +57,6 @@ Future main(List args) async { await session_test.main(args); await subscription_test.main(args); await user_test.main(args); - await realm_logger_test.main(args); tearDown(() { if (Invoker.current?.liveTest.state.result == test_api.Result.error || Invoker.current?.liveTest.state.result == test_api.Result.failure) { diff --git a/lib/src/app.dart b/lib/src/app.dart index 8d06c9449..449632874 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -176,7 +176,11 @@ class App implements Finalizable { /// Create an app with a particular [AppConfiguration]. This constructor should only be used on the main isolate and, /// ideally, only once as soon as the app starts. App(AppConfiguration configuration) : _handle = _createApp(configuration) { - if (Isolate.current.debugName != 'main') { + // This is not foolproof, but could point people to errors they may have in their app. Realm apps are cached natively, so calling App(config) + // on a background isolate will not recreate the app. Instead, users should construct the app on the main isolate and then call getById on the + // background isolates. This check will log a warning if the isolate name is != 'main' and doesn't start with 'test/' since dart test will + // construct a new isolate per file and we don't want to log excessively in unit test projects. + if (Isolate.current.debugName != 'main' && Isolate.current.debugName?.startsWith('test/') == false) { Realm.logger.log(RealmLogLevel.warn, "App constructor called on Isolate ${Isolate.current.debugName} which doesn't appear to be the main isolate. If you need an app instance on a background isolate use App.getById after constructing the App on the main isolate."); } diff --git a/lib/src/cli/atlas_apps/baas_client.dart b/lib/src/cli/atlas_apps/baas_client.dart index f0d78f972..b5b4df118 100644 --- a/lib/src/cli/atlas_apps/baas_client.dart +++ b/lib/src/cli/atlas_apps/baas_client.dart @@ -16,9 +16,34 @@ // //////////////////////////////////////////////////////////////////////////////// +import 'package:collection/collection.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; +class BaasAuthHelper { + static const String _appId = 'baas-container-service-autzb'; + final String _apiKey; + final String _location; + + BaasAuthHelper(this._apiKey) : _location = 'https://us-east-1.aws.data.mongodb-api.com'; + + Future callEndpoint(String name, {Object? body, Map? query, bool isPost = true}) async { + var url = '$_location/app/$_appId/endpoint/$name'; + if (query != null) { + url = '$url?${query.entries.map((kvp) => '${kvp.key}=${kvp.value}').join('&')}'; + } + final headers = {'apiKey': _apiKey}; + final response = isPost ? await http.post(Uri.parse(url), headers: headers, body: jsonEncode(body)) : await http.get(Uri.parse(url), headers: headers); + + return BaasClient._decodeResponse(response); + } + + Future getUserId() async { + final response = await callEndpoint('userinfo', isPost: false) as Map; + return response['id'] as String; + } +} + class BaasClient { static const String _confirmFuncSource = '''exports = async ({ token, tokenId, username }) => { // process the confirm token, tokenId and username @@ -86,25 +111,25 @@ class BaasClient { static const String defaultAppName = "flexible"; - final String _baseUrl; + final String _adminApiUrl; final String? _clusterName; final Map _headers; final String _appSuffix; - final String _sharedAppSuffix; + + final String baseUrl; late String _groupId; late String publicRSAKey = ''; - BaasClient._(String baseUrl, String? differentiator, [this._clusterName]) - : _baseUrl = '$baseUrl/api/admin/v3.0', + BaasClient._(this.baseUrl, String differentiator, [this._clusterName]) + : _adminApiUrl = '$baseUrl/api/admin/v3.0', _headers = {'Accept': 'application/json'}, - _appSuffix = '-${shortenDifferentiator(differentiator ?? 'local')}-$_clusterName', - _sharedAppSuffix = '-shared-$_clusterName'; + _appSuffix = '-${shortenDifferentiator(differentiator)}${_clusterName == null ? '' : '-$_clusterName'}'; /// A client that imports apps in a MongoDB Atlas docker image. See https://github.com/realm/ci/tree/master/realm/docker/mongodb-realm /// for instructions on how to set it up. /// @nodoc - static Future docker(String baseUrl, String? differentiator) async { + static Future docker(String baseUrl, String differentiator) async { final result = BaasClient._(baseUrl, differentiator); await result._authenticate('local-userpass', '{ "username": "unique_user@domain.com", "password": "password" }'); @@ -117,9 +142,105 @@ class BaasClient { return result; } + static Future deleteContainer(String apiKey, String differentiator) async { + try { + print('Stopping all containers with differentiator $differentiator'); + final authHelper = BaasAuthHelper(apiKey); + final containers = await _getContainers(authHelper, differentiator: differentiator); + for (final container in containers) { + print('Stopping container ${container.id}'); + await authHelper.callEndpoint('stopContainer', query: {'id': container.id}); + print('Stopped container ${container.id}'); + } + return; + } catch (e) { + print('Failed to destroy container: $e'); + rethrow; + } + } + + static Future<(String httpUrl, String containerId)> getOrDeployContainer(String apiKey, String differentiator) async { + final authHelper = BaasAuthHelper(apiKey); + final existing = (await _getContainers(authHelper, differentiator: differentiator)).firstOrNull; + if (existing != null) { + print('Using existing BaaS container at ${existing.httpUrl}'); + return (existing.httpUrl, existing.id); + } + + print('Deploying new BaaS container... '); + final response = await authHelper.callEndpoint('startContainer', body: [ + {'key': 'DIFFERENTIATOR', 'value': differentiator} + ]) as Map; + final id = response['id'] as String; + + String? httpUrl; + while (httpUrl == null) { + await Future.delayed(Duration(seconds: 1)); + httpUrl = await _waitForContainer(authHelper, id); + } + + print('Deployed BaaS instance at $httpUrl'); + + return (httpUrl, id); + } + + static Future retry(Future Function() func, {int attempts = 5}) async { + while (attempts >= 0) { + try { + return await func(); + } catch (e) { + print('An error occurred: $e'); + if (--attempts == 0) { + rethrow; + } + } + } + + throw 'UNREACHABLE'; + } + + static Future> _getContainers(BaasAuthHelper helper, {String? differentiator}) async { + var result = (await helper.callEndpoint('listContainers', isPost: false) as List).map((e) => _ContainerInfo.fromJson(e)).whereNotNull(); + if (differentiator != null) { + final userId = await helper.getUserId(); + result = result.where((c) => c.creatorId == userId && c.tags['DIFFERENTIATOR'] == differentiator); + } + + return result.toList(); + } + + static Future _waitForContainer(BaasAuthHelper authHelper, String taskId) async { + try { + final containers = await _getContainers(authHelper); + final targetContainer = containers.firstWhereOrNull((c) => c.id == taskId); + if (targetContainer == null) { + print('$taskId is not found in container list. Retrying...'); + return null; + } + + if (!targetContainer.isRunning) { + print('$taskId status is ${targetContainer.lastStatus}. Retrying...'); + return null; + } + + final httpUrl = targetContainer.httpUrl; + + final response = await http.get(Uri.parse('$httpUrl/api/private/v1.0/version')); + if (response.statusCode > 300) { + print('$taskId version response is ${response.statusCode}. Retrying...'); + return null; + } + + return httpUrl; + } catch (e) { + print('Error waiting for container: $e'); + return null; + } + } + /// A client that imports apps to a MongoDB Atlas environment (typically realm-dev or realm-qa). /// @nodoc - static Future atlas(String baseUrl, String cluster, String apiKey, String privateApiKey, String groupId, String? differentiator) async { + static Future atlas(String baseUrl, String cluster, String apiKey, String privateApiKey, String groupId, String differentiator) async { final BaasClient result = BaasClient._(baseUrl, differentiator, cluster); await result._authenticate('mongodb-cloud', '{ "username": "$apiKey", "apiKey": "$privateApiKey" }'); @@ -133,34 +254,16 @@ class BaasClient { /// for [atlas] one, it will return only apps with suffix equal to the cluster name. If no apps exist, /// then it will create the test applications and return them. /// @nodoc - Future> getOrCreateApps() async { - final result = await _getExistingApps(); - await _createAppIfNotExists(result, defaultAppName, _appSuffix); - await _createAppIfNotExists(result, "autoConfirm", _sharedAppSuffix, confirmationType: "auto"); - await _createAppIfNotExists(result, "emailConfirm", _sharedAppSuffix, confirmationType: "email"); - return result; - } - - Future> getOrCreateSharedApps() async { - final result = await _getExistingApps(); - await _createAppIfNotExists(result, "autoConfirm", _sharedAppSuffix, confirmationType: "auto"); - await _createAppIfNotExists(result, "emailConfirm", _sharedAppSuffix, confirmationType: "email"); - return result; - } - - Future> _getExistingApps() async { - final result = {}; + Future> getOrCreateApps() async { var apps = await _getApps(); - if (apps.isNotEmpty) { - for (final app in apps) { - result[app.name] = app; - } - } - return result; + await _createAppIfNotExists(apps, defaultAppName, _appSuffix); + await _createAppIfNotExists(apps, "autoConfirm", _appSuffix, confirmationType: "auto"); + await _createAppIfNotExists(apps, "emailConfirm", _appSuffix, confirmationType: "email"); + return apps; } Future waitForInitialSync(BaasApp app) async { - while (!await _isSyncComplete(app)) { + while (!await _isSyncComplete(app.appId)) { print('Initial sync for ${app.name} is incomplete. Waiting 5 seconds.'); await Future.delayed(Duration(seconds: 5)); } @@ -168,16 +271,16 @@ class BaasClient { print('Initial sync for ${app.name} is complete.'); } - Future _createAppIfNotExists(Map existingApps, String appName, String appSuffix, {String? confirmationType}) async { - final existingApp = existingApps[appName]; + Future _createAppIfNotExists(List existingApps, String appName, String appSuffix, {String? confirmationType}) async { + final existingApp = existingApps.firstWhereOrNull((a) => a.name == appName); if (existingApp == null) { - existingApps[appName] = await _createApp(appName, appSuffix, confirmationType: confirmationType); + existingApps.add(await _createApp(appName, appSuffix, confirmationType: confirmationType)); } } - Future _isSyncComplete(BaasApp app) async { + Future _isSyncComplete(String appId) async { try { - final response = await _get('groups/$_groupId/apps/$app/sync/progress'); + final response = await _get('groups/$_groupId/apps/$appId/sync/progress'); Map progressInfo = response['progress']; for (final key in progressInfo.keys) { @@ -203,12 +306,10 @@ class BaasClient { final String appName; if (name.endsWith(_appSuffix)) { appName = name.substring(0, name.length - _appSuffix.length); - } else if (name.endsWith(_sharedAppSuffix)) { - appName = name.substring(0, name.length - _sharedAppSuffix.length); } else { return null; } - return BaasApp(doc['_id'] as String, doc['client_app_id'] as String, appName, name); + return BaasApp(appId: doc['_id'] as String, clientAppId: doc['client_app_id'] as String, name: appName, uniqueName: name, isNewDeployment: false); }) .where((doc) => doc != null) .map((doc) => doc!) @@ -217,15 +318,14 @@ class BaasClient { Future updateAppConfirmFunction(String name, [String? source]) async { final uniqueName = "$name$_appSuffix"; - final uniqueSharedAppName = "$name$_sharedAppSuffix"; final dynamic docs = await _get('groups/$_groupId/apps'); dynamic doc = docs.firstWhere((dynamic d) { - return d["name"] == uniqueName || d["name"] == uniqueSharedAppName; + return d["name"] == uniqueName; }, orElse: () => throw Exception("BAAS app not found")); final appId = doc['_id'] as String; final appUniqueName = doc['name'] as String; final clientAppId = doc['client_app_id'] as String; - final app = BaasApp(appId, clientAppId, name, appUniqueName); + final app = BaasApp(appId: appId, clientAppId: clientAppId, name: name, uniqueName: appUniqueName, isNewDeployment: false); final dynamic functions = await _get('groups/$_groupId/apps/$appId/functions'); dynamic function = functions.firstWhere((dynamic f) => f["name"] == "confirmFunc", orElse: () => throw Exception("Func 'confirmFunc' not found")); @@ -241,10 +341,8 @@ class BaasClient { BaasApp? app; try { final dynamic doc = await _post('groups/$_groupId/apps', '{ "name": "$uniqueName" }'); - final appId = doc['_id'] as String; - final clientAppId = doc['client_app_id'] as String; - app = BaasApp(appId, clientAppId, name, uniqueName); + app = BaasApp(appId: doc['_id'] as String, clientAppId: doc['client_app_id'] as String, name: name, uniqueName: uniqueName, isNewDeployment: true); final confirmFuncId = await _createFunction(app, 'confirmFunc', _confirmFuncSource); final resetFuncId = await _createFunction(app, 'resetFunc', _resetFuncSource); @@ -273,7 +371,7 @@ class BaasClient { if (publicRSAKey.isNotEmpty) { String publicRSAKeyEncoded = jsonEncode(publicRSAKey); - final dynamic createSecretResult = await _post('groups/$_groupId/apps/$appId/secrets', '{"name":"rsPublicKey","value":$publicRSAKeyEncoded}'); + final dynamic createSecretResult = await _post('groups/$_groupId/apps/$app/secrets', '{"name":"rsPublicKey","value":$publicRSAKeyEncoded}'); String keyName = createSecretResult['name'] as String; await enableProvider(app, 'custom-token', config: '''{ @@ -336,7 +434,7 @@ class BaasClient { }'''); const facebookSecret = "876750ac6d06618b323dee591602897f"; - final dynamic createFacebookSecretResult = await _post('groups/$_groupId/apps/$appId/secrets', '{"name":"facebookSecret","value":"$facebookSecret"}'); + final dynamic createFacebookSecretResult = await _post('groups/$_groupId/apps/$app/secrets', '{"name":"facebookSecret","value":"$facebookSecret"}'); String facebookClientSecretKeyName = createFacebookSecretResult['name'] as String; await enableProvider(app, 'oauth2-facebook', config: '''{ "clientId": "1265617494254819" @@ -409,10 +507,10 @@ class BaasClient { ] }''', ); - await _put('groups/$_groupId/apps/$appId/sync/config', '{ "development_mode_enabled": true }'); + await _put('groups/$_groupId/apps/$app/sync/config', '{ "development_mode_enabled": true }'); //create email/password user for tests - final dynamic createUserResult = await _post('groups/$_groupId/apps/$appId/users', '{"email": "realm-test@realm.io", "password":"123456"}'); + final dynamic createUserResult = await _post('groups/$_groupId/apps/$app/users', '{"email": "realm-test@realm.io", "password":"123456"}'); print("Create user result: $createUserResult"); } catch (error) { print(error); @@ -453,10 +551,10 @@ class BaasClient { } } - Future createApiKey(BaasApp app, String name, bool enabled) async { - final dynamic result = await _post('groups/$_groupId/apps/${app.appId}/api_keys', '{ "name":"$name" }'); + Future createApiKey(String appId, String name, bool enabled) async { + final dynamic result = await _post('groups/$_groupId/apps/$appId/api_keys', '{ "name":"$name" }'); if (!enabled) { - await _put('groups/$_groupId/apps/${app.appId}/api_keys/${result['_id']}/disable', ''); + await _put('groups/$_groupId/apps/$appId/api_keys/${result['_id']}/disable', ''); } return result['key'] as String; @@ -549,7 +647,7 @@ class BaasClient { } Uri _getUri(String relativePath) { - return Uri.parse('$_baseUrl/$relativePath'); + return Uri.parse('$_adminApiUrl/$relativePath'); } Future _post(String relativePath, String payload) async { @@ -577,7 +675,7 @@ class BaasClient { return _decodeResponse(response, payload); } - dynamic _decodeResponse(http.Response response, [String? payload]) { + static dynamic _decodeResponse(http.Response response, [String? payload]) { if (response.statusCode > 399 || response.statusCode < 200) { throw Exception('Failed to ${response.request?.method} ${response.request?.url}: ${response.statusCode} ${response.body}. Body: $payload'); } @@ -599,20 +697,17 @@ class BaasClient { Future setAutomaticRecoveryEnabled(String name, bool enable) async { final uniqueName = "$name$_appSuffix"; - final uniqueSharedAppName = "$name$_sharedAppSuffix"; final dynamic docs = await _get('groups/$_groupId/apps'); dynamic doc = docs.firstWhere((dynamic d) { - return d["name"] == uniqueName || d["name"] == uniqueSharedAppName; + return d["name"] == uniqueName; }, orElse: () => throw Exception("BAAS app not found")); - final appId = doc['_id'] as String; - final appUniqueName = doc['name'] as String; - final clientAppId = doc['client_app_id'] as String; - final app = BaasApp(appId, clientAppId, name, appUniqueName); + final app = BaasApp( + appId: doc['_id'] as String, clientAppId: doc['client_app_id'] as String, name: name, uniqueName: doc['name'] as String, isNewDeployment: false); - final dynamic services = await _get('groups/$_groupId/apps/$appId/services'); + final dynamic services = await _get('groups/$_groupId/apps/$app/services'); dynamic service = services.firstWhere((dynamic s) => s["name"] == "BackingDB", orElse: () => throw Exception("Func 'confirmFunc' not found")); final mongoServiceId = service['_id'] as String; - final dynamic configDocs = await _get('groups/$_groupId/apps/$appId/services/$mongoServiceId/config'); + final dynamic configDocs = await _get('groups/$_groupId/apps/$app/services/$mongoServiceId/config'); final dynamic flexibleSync = configDocs['flexible_sync']; final dynamic clusterName = configDocs['clusterName']; flexibleSync["is_recovery_mode_disabled"] = !enable; @@ -624,19 +719,46 @@ class BaasClient { } } +class _ContainerInfo { + final String id; + bool get isRunning => lastStatus == 'RUNNING'; + final String httpUrl; + final String lastStatus; + final Map tags; + final String creatorId; + + _ContainerInfo._(this.id, this.httpUrl, this.lastStatus, this.tags, this.creatorId); + + static _ContainerInfo? fromJson(Map json) { + final httpUrl = json['httpUrl'] as String?; + if (httpUrl == null) { + return null; + } + + final id = json['id'] as String; + final lastStatus = json['lastStatus']; + final tags = {for (var v in json['tags'] as List) v['key'] as String: v['value'] as String}; + final creatorId = json['creatorId'] as String; + + return _ContainerInfo._(id, httpUrl, lastStatus, tags, creatorId); + } +} + class BaasApp { final String appId; final String clientAppId; final String name; final String uniqueName; + final bool isNewDeployment; Object? error; - BaasApp(this.appId, this.clientAppId, this.name, this.uniqueName); + BaasApp({required this.appId, required this.clientAppId, required this.name, required this.uniqueName, required this.isNewDeployment}); BaasApp._empty(this.name) : appId = "", clientAppId = "", - uniqueName = ""; + uniqueName = "", + isNewDeployment = false; @override String toString() { diff --git a/lib/src/cli/atlas_apps/deleteapps_command.dart b/lib/src/cli/atlas_apps/deleteapps_command.dart index 4cd893658..64a6802a4 100644 --- a/lib/src/cli/atlas_apps/deleteapps_command.dart +++ b/lib/src/cli/atlas_apps/deleteapps_command.dart @@ -56,13 +56,22 @@ class DeleteAppsCommand extends Command { abort('--project-id must be supplied when --atlas-cluster is not set'); } } + + if (options.baasaasApiKey == null && options.baasUrl == null) { + abort('--baas-url must be supplied when --baasaas-api-key is null'); + } + final differentiator = options.differentiator ?? 'local'; - final client = await (options.atlasCluster == null - ? BaasClient.docker(options.baasUrl, differentiator) - : BaasClient.atlas(options.baasUrl, options.atlasCluster!, options.apiKey!, options.privateApiKey!, options.projectId!, differentiator)); + if (options.baasaasApiKey != null) { + await BaasClient.retry(() => BaasClient.deleteContainer(options.baasaasApiKey!, differentiator)); + } else { + final client = await (options.atlasCluster == null + ? BaasClient.docker(options.baasUrl!, differentiator) + : BaasClient.atlas(options.baasUrl!, options.atlasCluster!, options.apiKey!, options.privateApiKey!, options.projectId!, differentiator)); - await client.deleteApps(); + await client.deleteApps(); + } } void abort(String error) { diff --git a/lib/src/cli/atlas_apps/deployapps_command.dart b/lib/src/cli/atlas_apps/deployapps_command.dart index f155466da..8e3b72414 100644 --- a/lib/src/cli/atlas_apps/deployapps_command.dart +++ b/lib/src/cli/atlas_apps/deployapps_command.dart @@ -56,36 +56,57 @@ RwIDAQAB if (options.atlasCluster != null) { if (options.apiKey == null) { - abort('--api-key must be supplied when --atlas-cluster is not set'); + abort('--api-key must be supplied when --atlas-cluster is set'); } if (options.privateApiKey == null) { - abort('--private-api-key must be supplied when --atlas-cluster is not set'); + abort('--private-api-key must be supplied when --atlas-cluster is set'); } if (options.projectId == null) { - abort('--project-id must be supplied when --atlas-cluster is not set'); + abort('--project-id must be supplied when --atlas-cluster is set'); } + + if (options.baasaasApiKey != null) { + abort('--baasaas-api-key cannot be used when --atlas-cluster is set'); + } + } + + if (options.baasaasApiKey == null && options.baasUrl == null) { + abort('--baas-url must be supplied when --baasaas-api-key is null'); + } + + final differentiator = options.differentiator ?? 'local'; + + late String baasUrl; + if (options.baasaasApiKey != null) { + late String containerId; + (baasUrl, containerId) = await BaasClient.getOrDeployContainer(options.baasaasApiKey!, differentiator); + await File('baasurl').writeAsString(baasUrl); + await File('containerid').writeAsString(containerId); + print('BaasUrl: $baasUrl'); + } else { + baasUrl = options.baasUrl!; } - final differentiator = options.differentiator ?? 'shared'; try { final client = await (options.atlasCluster == null - ? BaasClient.docker(options.baasUrl, differentiator) - : BaasClient.atlas(options.baasUrl, options.atlasCluster!, options.apiKey!, options.privateApiKey!, options.projectId!, differentiator)); + ? BaasClient.docker(baasUrl, differentiator) + : BaasClient.atlas(baasUrl, options.atlasCluster!, options.apiKey!, options.privateApiKey!, options.projectId!, differentiator)); client.publicRSAKey = publicRSAKeyForJWTValidation; - var apps = await client.getOrCreateSharedApps(); + var apps = await client.getOrCreateApps(); print('App import is complete. There are: ${apps.length} apps on the server:'); List listApps = []; - apps.forEach((_, value) { + for (var value in apps) { print(" App '${value.name}': '${value.clientAppId}'"); if (value.error != null) { print(value.error!); } listApps.add(value.appId); - }); + } print("appIds: "); print(listApps.join(",")); + exit(0); } catch (error) { print(error); } diff --git a/lib/src/cli/atlas_apps/options.dart b/lib/src/cli/atlas_apps/options.dart index 46cbd6cfd..b98658b83 100644 --- a/lib/src/cli/atlas_apps/options.dart +++ b/lib/src/cli/atlas_apps/options.dart @@ -22,8 +22,8 @@ part 'options.g.dart'; @CliOptions() class Options { - @CliOption(help: 'Url for MongoDB Atlas.', defaultsTo: 'http://localhost:9090') - final String baasUrl; + @CliOption(help: 'Url for MongoDB Atlas.') + final String? baasUrl; @CliOption(help: 'The database prefix that will be used for the sync service.') final String? differentiator; @@ -40,7 +40,10 @@ class Options { @CliOption(help: 'The Atlas project id to use for the import. Only used if atlas-cluster is specified.') final String? projectId; - Options(this.baasUrl, {this.atlasCluster, this.apiKey, this.privateApiKey, this.projectId, this.differentiator}); + @CliOption(help: 'API key to use with BaaSaaS to spawn a new container and create apps in it.', name: 'baasaas-api-key') + final String? baasaasApiKey; + + Options({this.baasUrl, this.atlasCluster, this.apiKey, this.privateApiKey, this.projectId, this.differentiator, this.baasaasApiKey}); } String get usage => _$parserForOptions.usage; diff --git a/lib/src/cli/atlas_apps/options.g.dart b/lib/src/cli/atlas_apps/options.g.dart index 10c4720f9..a6cd50993 100644 --- a/lib/src/cli/atlas_apps/options.g.dart +++ b/lib/src/cli/atlas_apps/options.g.dart @@ -7,19 +7,19 @@ part of 'options.dart'; // ************************************************************************** Options _$parseOptionsResult(ArgResults result) => Options( - result['baas-url'] as String, + baasUrl: result['baas-url'] as String?, atlasCluster: result['atlas-cluster'] as String?, apiKey: result['api-key'] as String?, privateApiKey: result['private-api-key'] as String?, projectId: result['project-id'] as String?, differentiator: result['differentiator'] as String?, + baasaasApiKey: result['baasaas-api-key'] as String?, ); ArgParser _$populateOptionsParser(ArgParser parser) => parser ..addOption( 'baas-url', help: 'Url for MongoDB Atlas.', - defaultsTo: 'http://localhost:9090', ) ..addOption( 'differentiator', @@ -43,6 +43,11 @@ ArgParser _$populateOptionsParser(ArgParser parser) => parser 'project-id', help: 'The Atlas project id to use for the import. Only used if atlas-cluster is specified.', + ) + ..addOption( + 'baasaas-api-key', + help: + 'API key to use with BaaSaaS to spawn a new container and create apps in it.', ); final _$parserForOptions = _$populateOptionsParser(ArgParser()); diff --git a/test/baas_helper.dart b/test/baas_helper.dart new file mode 100644 index 000000000..438398acc --- /dev/null +++ b/test/baas_helper.dart @@ -0,0 +1,257 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:test/test.dart' as testing; + +import '../lib/realm.dart'; +import '../lib/src/cli/atlas_apps/baas_client.dart'; +import '../lib/src/native/realm_core.dart'; + +const String argBaasUrl = "BAAS_URL"; +const String argBaasCluster = "BAAS_CLUSTER"; +const String argBaasApiKey = "BAAS_API_KEY"; +const String argBaasPrivateApiKey = "BAAS_PRIVATE_API_KEY"; +const String argBaasProjectId = "BAAS_PROJECT_ID"; +const String argDifferentiator = "BAAS_DIFFERENTIATOR"; +const String argBaasaasApiKey = "BAAS_BAASAAS_API_KEY"; + +const String publicRSAKeyForJWTValidation = '''-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvNHHs8T0AHD7SJ+CKvVR +leeJa4wqYTnaVYV+5bX9FmFXVoN+vHbMLEteMvSw4L3kSRZdcqxY7cTuhlpAvkXP +Yq6qSI+bW8T4jGW963uCc83UhVMx4MH/PzipAlfcPjVO2u4c+dmpgZQpgEmA467u +tauXUhmTsGpgNg2Gvc61B7Ny4LphshsyrfaJ9WjA/NM6LOmEBW3JPNcVG2qyU+gt +O8BM8KOSx9wGyoGs4+OusvRkJizhPaIwa3FInLs4r+xZW9Bp6RndsmVECtvXRv5d +87ztpg6o3DZJRmTp2lAnkNLmxXlFkOSNIwiT3qqyRZOh4DuxPOpfg9K+vtFmRdEJ +RwIDAQAB +-----END PUBLIC KEY-----'''; + +Map parseTestArguments(List? arguments) { + Map testArgs = {}; + final parser = ArgParser() + ..addOption("name") + ..addOption(argBaasUrl) + ..addOption(argBaasCluster) + ..addOption(argBaasApiKey) + ..addOption(argBaasPrivateApiKey) + ..addOption(argBaasProjectId) + ..addOption(argDifferentiator) + ..addOption(argBaasaasApiKey); + + final result = parser.parse(arguments ?? []); + testArgs + ..addArgument(result, "name") + ..addArgument(result, argBaasUrl) + ..addArgument(result, argBaasCluster) + ..addArgument(result, argBaasApiKey) + ..addArgument(result, argBaasPrivateApiKey) + ..addArgument(result, argBaasProjectId) + ..addArgument(result, argDifferentiator) + ..addArgument(result, argBaasaasApiKey); + + return testArgs; +} + +extension on Map { + void addArgument(ArgResults parsedResult, String argName) { + final value = parsedResult.wasParsed(argName) ? parsedResult[argName]?.toString() : Platform.environment[argName]; + if (value != null && value.isNotEmpty) { + this[argName] = value; + } + } +} + +enum AppNames { + flexible, + + // For application with name 'autoConfirm' and with confirmationType = 'auto' + // all the usernames are automatically confirmed. + autoConfirm, + + emailConfirm, +} + +class BaasHelper { + final BaasClient _baasClient; + final _baasApps = {}; + + String get baseUrl => _baasClient.baseUrl; + + static Object? _error; + + static Future setupBaas(Map args) async { + try { + final client = await _setupClient(args); + if (client == null) { + return null; + } + + final result = BaasHelper._(client); + + await result._setupApps(); + + return result; + } catch (e) { + print(e); + _error = e; + rethrow; + } + } + + static bool shouldRunBaasTests(Map args) { + return args[argBaasaasApiKey] != null || args[argBaasUrl] != null; + } + + BaasHelper._(this._baasClient); + + static Future _setupClient(Map args) async { + var baasUrl = args[argBaasUrl]; + final differentiator = args[argDifferentiator] ?? 'local'; + if (baasUrl == null) { + final baasaasApiKey = args[argBaasaasApiKey]; + if (baasaasApiKey != null) { + if (args[argBaasCluster] != null) { + throw "$argBaasaasApiKey can't be combined with $argBaasCluster"; + } + + (baasUrl, _) = await BaasClient.retry(() => BaasClient.getOrDeployContainer(baasaasApiKey, differentiator)); + } + } + + if (baasUrl == null) { + return null; + } + + final cluster = args[argBaasCluster]; + final apiKey = args[argBaasApiKey]; + final privateApiKey = args[argBaasPrivateApiKey]; + final projectId = args[argBaasProjectId]; + + final client = await BaasClient.retry(() => (cluster == null + ? BaasClient.docker(baasUrl!, differentiator) + : BaasClient.atlas(baasUrl!, cluster, apiKey!, privateApiKey!, projectId!, differentiator))); + + client.publicRSAKey = publicRSAKeyForJWTValidation; + return client; + } + + Future _setupApps() async { + try { + final apps = await _baasClient.getOrCreateApps(); + + for (final app in apps) { + _baasApps[app.name] = app; + if (app.name == AppNames.flexible.name && app.isNewDeployment) { + await _waitForInitialSync(app); + } + } + } catch (error) { + print(error); + _error = error; + } + } + + Future _waitForInitialSync(BaasApp app) async { + while (true) { + try { + print('Validating initial sync is complete...'); + await _baasClient.waitForInitialSync(app); + return; + } catch (e) { + print(e); + } finally { + realmCore.clearCachedApps(); + } + } + } + + Future createServerApiKey(App app, String name, {bool enabled = true}) async { + final baasApp = _baasApps.values.firstWhere((ba) => ba.clientAppId == app.id); + return await _baasClient.createApiKey(baasApp.appId, name, enabled); + } + + static void throwIfSetupFailed() { + if (_error != null) { + throw _error!; + } + } + + void printSplunkLogLink(AppNames appName, String? uriVariable) { + if (uriVariable == null) { + return; + } + + final app = _baasApps[appName.name] ?? (throw RealmError("No BAAS apps")); + final baasUri = Uri.parse(uriVariable); + + testing.printOnFailure("App service name: ${app.uniqueName}"); + final host = baasUri.host.endsWith('-qa.mongodb.com') ? "-qa" : ""; + final splunk = Uri.encodeFull( + "https://splunk.corp.mongodb.com/en-US/app/search/search?q=search index=baas$host \"${app.uniqueName}-*\" | reverse | top error msg&earliest=-7d&latest=now&display.general.type=visualizations"); + testing.printOnFailure("Splunk logs: $splunk"); + } + + Future getAppConfig({AppNames appName = AppNames.flexible}) => _getAppConfig(appName.name); + + Future _getAppConfig(String appName) async { + final app = _baasApps[appName] ?? + _baasApps.values.firstWhere((element) => element.name == BaasClient.defaultAppName, orElse: () => throw RealmError("No BAAS apps")); + if (app.error != null) { + throw app.error!; + } + + final temporaryDir = await Directory.systemTemp.createTemp('realm_test_'); + return AppConfiguration( + app.clientAppId, + baseUrl: Uri.parse(baseUrl), + baseFilePath: temporaryDir, + maxConnectionTimeout: Duration(minutes: 10), + defaultRequestTimeout: Duration(minutes: 7), + ); + } + + String getClientAppId({AppNames appName = AppNames.flexible}) => _baasApps[appName.name]!.clientAppId; + + Future disableAutoRecoveryForApp(AppNames appName) async { + await _baasClient.setAutomaticRecoveryEnabled(appName.name, false); + } + + Future enableAutoRecoveryForApp(AppNames appName) async { + await _baasClient.setAutomaticRecoveryEnabled(appName.name, true); + } + + Future triggerClientReset(Realm realm, {bool restartSession = true}) async { + final config = realm.config; + if (config is! FlexibleSyncConfiguration) { + throw RealmError('This should only be invoked for sync realms'); + } + + final session = realm.syncSession; + if (restartSession) { + session.pause(); + } + + final userId = config.user.id; + final appId = _baasApps.values.firstWhere((element) => element.clientAppId == config.user.app.id).appId; + + for (var i = 0; i < 5; i++) { + try { + final result = await config.user.functions.call('triggerClientResetOnSyncServer', [userId, appId]) as Map; + if (result['status'] != 'success') { + throw 'Unsuccesful status: ${result['status']}'; + } + break; + } catch (e) { + if (i == 4) { + rethrow; + } + + print('Failed to trigger client reset: $e'); + await Future.delayed(Duration(seconds: i)); + } + } + + if (restartSession) { + session.resume(); + } + } +} diff --git a/test/client_reset_test.dart b/test/client_reset_test.dart index 56391748f..e1e8b6b0a 100644 --- a/test/client_reset_test.dart +++ b/test/client_reset_test.dart @@ -81,7 +81,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); final clientResetFuture = resetCompleter.future.wait(defaultWaitTimeout, "ManualRecoveryHandler is not reported."); await expectLater(clientResetFuture, throws('Bad client file identifier')); }); @@ -108,7 +108,7 @@ Future main([List? args]) async { clientResetError.resetRealm(); }, test: (error) => error is ClientResetError); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); await resetRealmFuture.wait(defaultWaitTimeout, "ManualRecoveryHandler is not reported."); @@ -136,7 +136,7 @@ Future main([List? args]) async { return clientResetError.resetRealm(); }, test: (error) => error is ClientResetError); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); expect(await resetRealmFuture.timeout(defaultWaitTimeout), !Platform.isWindows); expect(File(config.path).existsSync(), Platform.isWindows); // posix and windows semantics are different @@ -162,7 +162,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); final clientResetFuture = onManualResetFallback.future.wait(defaultWaitTimeout, "onManualResetFallback is not reported."); await expectLater( @@ -191,7 +191,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); final clientResetFuture = onManualResetFallback.future.wait(defaultWaitTimeout, "onManualResetFallback is not reported."); await expectLater( @@ -223,7 +223,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); await onBeforeCompleter.future.timeout(defaultWaitTimeout, onTimeout: () => throw TimeoutException("onBeforeReset is not reported")); await onAfterCompleter.future.timeout(defaultWaitTimeout, onTimeout: () => throw TimeoutException("onAfterReset is not reported.")); @@ -281,9 +281,9 @@ Future main([List? args]) async { await waitForCondition(() => notifications.length == 1, timeout: Duration(seconds: 3)); if (shouldDisableAutoRecoveryForApp) { - await disableAutoRecoveryForApp(baasAppName); + await baasHelper!.disableAutoRecoveryForApp(baasAppName); } - await triggerClientReset(realm, restartSession: false); + await baasHelper!.triggerClientReset(realm, restartSession: false); realm.syncSession.resume(); await onAfterCompleter.future.wait(defaultWaitTimeout, "Neither onAfterDiscard nor onManualResetFallback is reported."); @@ -297,7 +297,7 @@ Future main([List? args]) async { expect(notifications.firstWhere((n) => n.deleted.isNotEmpty), isNotNull); } finally { if (shouldDisableAutoRecoveryForApp) { - await enableAutoRecoveryForApp(baasAppName); + await baasHelper!.enableAutoRecoveryForApp(baasAppName); } } }); @@ -347,7 +347,7 @@ Future main([List? args]) async { realm.syncSession.pause(); realm.write(() => realm.add(Product(maybeId, "maybe synced"))); - await triggerClientReset(realm, restartSession: false); + await baasHelper!.triggerClientReset(realm, restartSession: false); realm.syncSession.resume(); await onAfterCompleter.future.wait(defaultWaitTimeout, "Neither onAfterDiscard, onAfterDiscard nor onManualResetFallback is reported."); }); @@ -381,9 +381,9 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await disableAutoRecoveryForApp(baasAppName); + await baasHelper!.disableAutoRecoveryForApp(baasAppName); try { - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); await onBeforeCompleter.future.wait(defaultWaitTimeout, "onBeforeReset is not reported."); await onAfterCompleter.future.wait(defaultWaitTimeout, "Neither onAfterRecovery nor onAfterDiscard is reported."); @@ -391,7 +391,7 @@ Future main([List? args]) async { expect(recovery, isFalse); expect(discard, isTrue); } finally { - await enableAutoRecoveryForApp(baasAppName); + await baasHelper!.enableAutoRecoveryForApp(baasAppName); } }); } @@ -423,7 +423,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); await onAfterCompleter.future.wait(defaultWaitTimeout, "onAfterReset is not reported."); @@ -465,7 +465,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); await manualResetFallbackCompleter.future.wait(defaultWaitTimeout, "onManualResetFallback is not reported."); @@ -535,11 +535,11 @@ Future main([List? args]) async { realmB.write(() => realmB.add(Task(task3Id))); - await triggerClientReset(realmA); + await baasHelper!.triggerClientReset(realmA); await realmA.syncSession.waitForUpload(); await afterRecoverCompleterA.future.wait(defaultWaitTimeout, "onAfterReset for realmA is not reported."); - await triggerClientReset(realmB, restartSession: false); + await baasHelper!.triggerClientReset(realmB, restartSession: false); realmB.syncSession.resume(); await realmB.syncSession.waitForUpload(); await afterRecoverCompleterB.future.wait(defaultWaitTimeout, "onAfterReset for realmB is not reported."); @@ -568,7 +568,7 @@ Future main([List? args]) async { final realm = await getRealmAsync(config); await realm.syncSession.waitForUpload(); - await triggerClientReset(realm); + await baasHelper!.triggerClientReset(realm); await resetCompleter.future.wait(defaultWaitTimeout, "ClientResetError is not reported."); expect(clientResetError.message, isNotEmpty); @@ -649,54 +649,8 @@ class Creator { } } -Future triggerClientReset(Realm realm, {bool restartSession = true}) async { - final config = realm.config; - if (config is! FlexibleSyncConfiguration) { - throw RealmError('This should only be invoked for sync realms'); - } - - final session = realm.syncSession; - if (restartSession) { - session.pause(); - } - - final userId = config.user.id; - final appId = baasApps.values.firstWhere((element) => element.clientAppId == config.user.app.id).appId; - - for (var i = 0; i < 5; i++) { - try { - final result = await config.user.functions.call('triggerClientResetOnSyncServer', [userId, appId]) as Map; - expect(result['status'], 'success'); - break; - } catch (e) { - if (i == 4) { - rethrow; - } - - print('Failed to trigger client reset: $e'); - await Future.delayed(Duration(seconds: i)); - } - } - - if (restartSession) { - session.resume(); - } -} - extension on Future { Future wait(Duration duration, [String message = "Timeout waiting a future to complete."]) { return timeout(duration, onTimeout: () => throw TimeoutException(message)); } } - -Future disableAutoRecoveryForApp(AppNames appName) async { - final client = baasClient ?? (throw StateError("No BAAS client")); - final baasAppName = baasApps[appName.name]!.name; - await client.setAutomaticRecoveryEnabled(baasAppName, false); -} - -Future enableAutoRecoveryForApp(AppNames appName) async { - final client = baasClient ?? (throw StateError("No BAAS client")); - final baasAppName = baasApps[appName.name]!.name; - await client.setAutomaticRecoveryEnabled(baasAppName, true); -} diff --git a/test/configuration_test.dart b/test/configuration_test.dart index 94a3884b3..a507700d0 100644 --- a/test/configuration_test.dart +++ b/test/configuration_test.dart @@ -100,9 +100,9 @@ Future main([List? args]) async { var customDefaultRealmPath = path.join((await Directory.systemTemp.createTemp()).path, Configuration.defaultRealmName); Configuration.defaultRealmPath = customDefaultRealmPath; - final appClientId = baasApps[AppNames.flexible.name]!.clientAppId; - final baasUrl = arguments[argBaasUrl]; - var appConfig = AppConfiguration(appClientId, baseUrl: Uri.parse(baasUrl!)); + final appClientId = baasHelper!.getClientAppId(appName: AppNames.flexible); + final baasUrl = baasHelper!.baseUrl; + var appConfig = AppConfiguration(appClientId, baseUrl: Uri.parse(baasUrl)); expect(appConfig.baseFilePath.path, path.dirname(customDefaultRealmPath)); var app = App(appConfig); diff --git a/test/indexed_test.dart b/test/indexed_test.dart index d92ff818f..1339f717f 100644 --- a/test/indexed_test.dart +++ b/test/indexed_test.dart @@ -87,8 +87,8 @@ const String lordOfTheFlies = 'Lord of the Flies'; const String wheelOfTime = 'The Wheel of Time'; const String silmarillion = 'The Silmarillion'; -void main([List? args]) { - setupTests(args); +Future main([List? args]) async { + await setupTests(args); intFactory(int i) => i.hashCode; boolFactory(int i) => i % 2 == 0; @@ -98,13 +98,17 @@ void main([List? args]) { uuidFactory(int i) => Uuid.fromBytes(Uint8List(16).buffer..asByteData().setInt64(0, i.hashCode)); // skip timestamp for now, as timestamps are not indexed properly it seems - final indexedTestData = [('anInt', intFactory), ('string', stringFactory), ('objectId', objectIdFactory), ('uuid', uuidFactory)]; + final indexedTestData = [ + (name: 'anInt', factory: intFactory), + (name: 'string', factory: stringFactory), + (name: 'objectId', factory: objectIdFactory), + (name: 'uuid', factory: uuidFactory) + ]; for (final testCase in indexedTestData) { - test('Indexed faster: ${testCase.$1}', () { + test('Indexed faster: ${testCase.name}', () { final config = Configuration.local([WithIndexes.schema, NoIndexes.schema]); - Realm.deleteRealm(config.path); - final realm = Realm(config); + final realm = getRealm(config); const max = 100000; final allIndexed = realm.all(); final allNotIndexed = realm.all(); @@ -142,16 +146,17 @@ void main([List? args]) { expect(allNotIndexed.length, max); // Inefficient, but fast enough for this test - final searchOrder = (List.generate(max, (i) => i)..shuffle(Random(42))).map((i) => testCase.$2(i)).take(1000).toList(); + final halfMax = max ~/ 2; + final searchOrder = (List.generate(halfMax, (i) => halfMax + i)..shuffle(Random(42))).map((i) => testCase.factory(i)).take(1000).toList(); @pragma('vm:no-interrupts') Duration measureSpeed(RealmResults results) { - final queries = searchOrder.map((v) => results.query('${testCase.$1} == \$0', [v])).toList(); // pre-calculate queries + final queries = searchOrder.map((v) => results.query('${testCase.name} == \$0', [v])).toList(); // pre-calculate queries final found = []; final sw = Stopwatch()..start(); for (final q in queries) { - found.add(q.singleOrNull); // evaluate query + found.add(q.single); // evaluate query } final timing = sw.elapsed; @@ -166,7 +171,7 @@ void main([List? args]) { final lookupCount = searchOrder.length; display(Type type, Duration duration) { - print('$lookupCount lookups of ${'$type'.padRight(12)} on ${testCase.$1.padRight(10)} : ${duration.inMicroseconds ~/ lookupCount} us/lookup'); + print('$lookupCount lookups of ${'$type'.padRight(12)} on ${testCase.name.padRight(10)} : ${duration.inMicroseconds ~/ lookupCount} us/lookup'); } final indexedTime = measureSpeed(allIndexed); diff --git a/test/list_test.dart b/test/list_test.dart index f93dab8a7..d808c0e41 100644 --- a/test/list_test.dart +++ b/test/list_test.dart @@ -349,6 +349,7 @@ Future main([List? args]) async { op(list, i); } }); + realm.refresh(); } }); } diff --git a/test/test.dart b/test/test.dart index 592046525..f48ee9e56 100644 --- a/test/test.dart +++ b/test/test.dart @@ -27,12 +27,14 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as _path; import 'package:test/test.dart' hide test; import 'package:test/test.dart' as testing; -import 'package:args/args.dart'; import '../lib/realm.dart'; -import '../lib/src/cli/atlas_apps/baas_client.dart'; import '../lib/src/native/realm_core.dart'; import '../lib/src/configuration.dart'; +import 'baas_helper.dart'; + +export 'baas_helper.dart' show AppNames; + part 'test.g.dart'; @RealmModel() @@ -352,39 +354,12 @@ class _Symmetric { } String? testName; -Map arguments = {}; -final baasApps = {}; final _openRealms = Queue(); -const String argBaasUrl = "BAAS_URL"; -const String argBaasCluster = "BAAS_CLUSTER"; -const String argBaasApiKey = "BAAS_API_KEY"; -const String argBaasPrivateApiKey = "BAAS_PRIVATE_API_KEY"; -const String argBaasProjectId = "BAAS_PROJECT_ID"; -const String argDifferentiator = "BAAS_DIFFERENTIATOR"; String testUsername = "realm-test@realm.io"; String testPassword = "123456"; -const String publicRSAKeyForJWTValidation = '''-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvNHHs8T0AHD7SJ+CKvVR -leeJa4wqYTnaVYV+5bX9FmFXVoN+vHbMLEteMvSw4L3kSRZdcqxY7cTuhlpAvkXP -Yq6qSI+bW8T4jGW963uCc83UhVMx4MH/PzipAlfcPjVO2u4c+dmpgZQpgEmA467u -tauXUhmTsGpgNg2Gvc61B7Ny4LphshsyrfaJ9WjA/NM6LOmEBW3JPNcVG2qyU+gt -O8BM8KOSx9wGyoGs4+OusvRkJizhPaIwa3FInLs4r+xZW9Bp6RndsmVECtvXRv5d -87ztpg6o3DZJRmTp2lAnkNLmxXlFkOSNIwiT3qqyRZOh4DuxPOpfg9K+vtFmRdEJ -RwIDAQAB ------END PUBLIC KEY-----'''; final int encryptionKeySize = 64; -enum AppNames { - flexible, - - // For application with name 'autoConfirm' and with confirmationType = 'auto' - // all the usernames are automatically confirmed. - autoConfirm, - - emailConfirm, -} - const int maxInt = 9223372036854775807; const int minInt = -9223372036854775808; const int jsMaxInt = 9007199254740991; @@ -412,11 +387,16 @@ void xtest(String? name, dynamic Function() testFunction, {dynamic skip, Map _testArgs = {}; + Future setupTests(List? args) async { - arguments = parseTestArguments(args); - testName = arguments["name"]; + _testArgs = parseTestArguments(args); + testName = _testArgs["name"]; - setUpAll(() async => await (_baasSetupResult ??= setupBaas())); + setUpAll(() async { + baasHelper = await BaasHelper.setupBaas(_testArgs); + }); setUp(() { Realm.logger = Logger.detached('test run') @@ -568,95 +548,6 @@ Future tryDeleteRealm(String path) async { // throw Exception('Failed to delete realm at path $path. Did you forget to close it?'); } -Map parseTestArguments(List? arguments) { - Map testArgs = {}; - final parser = ArgParser() - ..addOption("name") - ..addOption(argBaasUrl) - ..addOption(argBaasCluster) - ..addOption(argBaasApiKey) - ..addOption(argBaasPrivateApiKey) - ..addOption(argBaasProjectId) - ..addOption(argDifferentiator); - - final result = parser.parse(arguments ?? []); - testArgs - ..addArgument(result, "name") - ..addArgument(result, argBaasUrl) - ..addArgument(result, argBaasCluster) - ..addArgument(result, argBaasApiKey) - ..addArgument(result, argBaasPrivateApiKey) - ..addArgument(result, argBaasProjectId) - ..addArgument(result, argDifferentiator); - - return testArgs; -} - -extension on Map { - void addArgument(ArgResults parsedResult, String argName) { - final value = parsedResult.wasParsed(argName) ? parsedResult[argName]?.toString() : Platform.environment[argName]; - if (value != null && value.isNotEmpty) { - this[argName] = value; - } - } -} - -BaasClient? baasClient; -Future? _baasSetupResult; - -Future setupBaas() async { - if (_baasSetupResult != null) { - return _baasSetupResult!; - } - - try { - final baasUrl = arguments[argBaasUrl]; - if (baasUrl == null) { - return true; - } - - final cluster = arguments[argBaasCluster]; - final apiKey = arguments[argBaasApiKey]; - final privateApiKey = arguments[argBaasPrivateApiKey]; - final projectId = arguments[argBaasProjectId]; - final differentiator = arguments[argDifferentiator]; - - final client = await (cluster == null - ? BaasClient.docker(baasUrl, differentiator) - : BaasClient.atlas(baasUrl, cluster, apiKey!, privateApiKey!, projectId!, differentiator)); - - client.publicRSAKey = publicRSAKeyForJWTValidation; - - final apps = await client.getOrCreateApps(); - baasApps.addAll(apps); - baasClient = client; - - await _waitForInitialSync(); - return true; - } catch (error) { - print(error); - return error; - } -} - -Future _waitForInitialSync() async { - while (true) { - try { - print('Validating initial sync is complete...'); - await baasClient!.waitForInitialSync(baasApps[AppNames.flexible.name]!); - final realm = await getIntegrationRealm(); - await realm.syncSession.waitForUpload(); - await baasClient!.waitForInitialSync(baasApps[AppNames.flexible.name]!); - return; - } catch (e) { - print(e); - await _waitForInitialSync(); - } finally { - clearCachedApps(); - } - } -} - @isTest Future baasTest( String name, @@ -664,53 +555,29 @@ Future baasTest( AppNames appName = AppNames.flexible, dynamic skip, }) async { - if (_baasSetupResult is Error) { - throw _baasSetupResult!; - } + BaasHelper.throwIfSetupFailed(); - final baasUri = arguments[argBaasUrl]; - skip = shouldSkip(baasUri, skip); + skip = shouldSkip(skip); test(name, () async { - printSplunkLogLink(appName, baasUri); - final config = await getAppConfig(appName: appName); + baasHelper!.printSplunkLogLink(appName, baasHelper?.baseUrl); + final config = await baasHelper!.getAppConfig(appName: appName); await testFunction(config); }, skip: skip); } -dynamic shouldSkip(String? baasUri, dynamic skip) { - final url = baasUri != null ? Uri.tryParse(baasUri) : null; - +dynamic shouldSkip(dynamic skip) { if (skip == null) { - skip = url == null ? "BAAS URL not present" : false; + skip = BaasHelper.shouldRunBaasTests(_testArgs) ? false : "BAAS URL not present"; } else if (skip is bool) { - if (url == null) skip = "BAAS URL not present"; + if (!BaasHelper.shouldRunBaasTests(_testArgs)) { + skip = "BAAS URL not present"; + } } return skip; } -Future getAppConfig({AppNames appName = AppNames.flexible}) => _getAppConfig(appName.name); - -Future _getAppConfig(String appName) async { - final baasUrl = arguments[argBaasUrl]; - - final app = - baasApps[appName] ?? baasApps.values.firstWhere((element) => element.name == BaasClient.defaultAppName, orElse: () => throw RealmError("No BAAS apps")); - if (app.error != null) { - throw app.error!; - } - - final temporaryDir = await Directory.systemTemp.createTemp('realm_test_'); - return AppConfiguration( - app.clientAppId, - baseUrl: Uri.parse(baasUrl!), - baseFilePath: temporaryDir, - maxConnectionTimeout: Duration(minutes: 10), - defaultRequestTimeout: Duration(minutes: 7), - ); -} - Future getIntegrationUser(App app) async { final email = 'realm_tests_do_autoverify_${generateRandomEmail()}'; final password = 'password'; @@ -723,14 +590,8 @@ Future getAnonymousUser(App app) { return app.logIn(Credentials.anonymous(reuseCredentials: false)); } -Future createServerApiKey(App app, String name, {bool enabled = true}) async { - final baasApp = baasApps.values.firstWhere((ba) => ba.clientAppId == app.id); - final client = baasClient ?? (throw StateError("No BAAS client")); - return await client.createApiKey(baasApp, name, enabled); -} - Future getIntegrationRealm({App? app, ObjectId? differentiator, AppConfiguration? appConfig}) async { - app ??= App(appConfig ?? await getAppConfig()); + app ??= App(appConfig ?? await baasHelper!.getAppConfig()); final user = await getIntegrationUser(app); final config = Configuration.flexibleSync(user, getSyncSchema())..sessionStopPolicy = SessionStopPolicy.immediately; @@ -844,22 +705,6 @@ extension StreamEx on Stream> { } } -void printSplunkLogLink(AppNames appName, String? uriVariable) { - if (uriVariable == null) { - return; - } - - final app = baasApps[appName.name] ?? - baasApps.values.firstWhere((element) => element.name == BaasClient.defaultAppName, orElse: () => throw RealmError("No BAAS apps")); - final baasUri = Uri.parse(uriVariable); - - testing.printOnFailure("App service name: ${app.uniqueName}"); - final host = baasUri.host.endsWith('-qa.mongodb.com') ? "-qa" : ""; - final splunk = Uri.encodeFull( - "https://splunk.corp.mongodb.com/en-US/app/search/search?q=search index=baas$host \"${app.uniqueName}-*\" | reverse | top error msg&earliest=-7d&latest=now&display.general.type=visualizations"); - testing.printOnFailure("Splunk logs: $splunk"); -} - /// Schema list for default app service /// used for all the flexible sync tests. /// The full list of schemas is required when creating diff --git a/test/user_test.dart b/test/user_test.dart index 0980af261..9fd98b375 100644 --- a/test/user_test.dart +++ b/test/user_test.dart @@ -441,7 +441,7 @@ Future main([List? args]) async { baasTest("Credentials.apiKey with server-generated can login user", (configuration) async { final app = App(configuration); - final apiKey = await createServerApiKey(app, ObjectId().toString()); + final apiKey = await baasHelper!.createServerApiKey(app, ObjectId().toString()); final credentials = Credentials.apiKey(apiKey); final apiKeyUser = await app.logIn(credentials); @@ -452,7 +452,7 @@ Future main([List? args]) async { baasTest("Credentials.apiKey with disabled server api key throws an error", (configuration) async { final app = App(configuration); - final apiKey = await createServerApiKey(app, ObjectId().toString(), enabled: false); + final apiKey = await baasHelper!.createServerApiKey(app, ObjectId().toString(), enabled: false); final credentials = Credentials.apiKey(apiKey); await expectLater(