diff --git a/.github/Dockerfile_PreBuild b/.github/Dockerfile_PreBuild index 11588d5ce9..df557a7748 100644 --- a/.github/Dockerfile_PreBuild +++ b/.github/Dockerfile_PreBuild @@ -1,7 +1,16 @@ -FROM gcr.io/distroless/java:11 +FROM eclipse-temurin:25-jre-alpine + +RUN addgroup -S obp && adduser -S -h /app -G obp obp # Copy OBP source code # Copy build artifact (JAR file) from maven build COPY /obp-api/target/obp-api.jar /app/obp-api.jar WORKDIR /app -CMD ["obp-api.jar"] \ No newline at end of file +USER obp +ENTRYPOINT ["java", \ + "--add-opens", "java.base/java.lang=ALL-UNNAMED", \ + "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", \ + "--add-opens", "java.base/java.io=ALL-UNNAMED", \ + "--add-opens", "java.base/java.util=ALL-UNNAMED", \ + "--add-opens", "java.base/java.util.concurrent=ALL-UNNAMED", \ + "-jar", "/app/obp-api.jar"] diff --git a/.github/workflows/build_container.yml b/.github/workflows/build_container.yml index e932d69315..5fd7156948 100644 --- a/.github/workflows/build_container.yml +++ b/.github/workflows/build_container.yml @@ -10,12 +10,12 @@ env: # --------------------------------------------------------------------------- # compile — compiles everything once, packages the JAR, uploads classes -# test — 4-way matrix downloads compiled output and runs a shard of tests +# test — 9-way matrix downloads compiled output and runs a shard of tests # docker — downloads compiled output, builds and pushes the container image # # Wall-clock target: # compile ~10 min (parallel with setup of test shards) -# tests ~8 min (4 shards in parallel after compile finishes) +# tests ~8 min (9 shards in parallel after compile finishes) # docker ~3 min (after all shards pass) # total ~21 min (vs ~30 min single-job) # --------------------------------------------------------------------------- @@ -30,11 +30,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 11 + - name: Set up JDK 25 uses: actions/setup-java@v4 with: - java-version: "11" - distribution: "adopt" + java-version: "25" + distribution: "temurin" cache: maven # caches ~/.m2/repository keyed on pom.xml hash - name: Setup production props @@ -42,6 +42,9 @@ jobs: cp obp-api/src/main/resources/props/sample.props.template \ obp-api/src/main/resources/props/production.default.props + - name: Lint — test-isolation (no setPropsValues at class/feature body) + run: python3 .github/scripts/check_test_isolation.py + - name: Compile and install (skip test execution) run: | # -DskipTests — compile test sources but do NOT run them @@ -74,26 +77,90 @@ jobs: path: push/ # -------------------------------------------------------------------------- - # Job 2: test (4-way matrix) + # Job 2: test (9-way matrix, mirrors build_pull_request.yml shard layout) # - # Shard assignment (based on actual clean-run timings): - # Shard 1 ~258s v4_0_0(258) - # Shard 2 ~267s v6_0_0(122) v5_0_0(42) v3_0_0(39) v2_1_0(35) v2_2_0(12) … - # Shard 3 ~252s v1_2_1(137) ResourceDocs(67) berlin(34) util(12) … - # Shard 4 ~232s v5_1_0(79) v3_1_0(65) http4sbridge(52) v7_0_0(45) … + catch-all + # Shard assignment (wall-clock on GitHub-hosted ubuntu-latest runners): + # Shard 1 ~157s v4_0_0 non-Dynamic (explicit class list, ~58 classes) + # Shard 2 ~257s v1_2_1 (single 6604-line suite, isolated) + # Shard 3 ~155s v6_0_0 only (isolated after v2_x moved to shard 7) + # Shard 4 ~183s v5_1_0 v5_0_0 v3_0_0 + # Shard 5 ~193s ResourceDocs v3_1_0 v1_4_0 v1_3_0 + # Shard 6 ~168s v7_0_0 http4sbridge UKOpenBanking + # Shard 7 ~280s model + views + customer + util + berlin + v2_x + # Shard 8 ~240s connector + auth + login + mgmt + metrics + catch-all + # Shard 9 ~110s v4_0_0 Dynamic* (6 heavy test classes) # -------------------------------------------------------------------------- test: needs: compile runs-on: ubuntu-latest + timeout-minutes: 35 strategy: fail-fast: false matrix: include: - shard: 1 - name: "v4 only (bottleneck pkg)" - # ~258s — single largest package, kept on its own shard + name: "v4 non-Dynamic" + # v4_0_0 split: non-Dynamic classes only (~58 classes). Dynamic* on shard 9. + # Listed by FQN so wildcardSuites doesn't also match Dynamic* classes. test_filter: >- - code.api.v4_0_0 + code.api.v4_0_0.AccountAccessTest + code.api.v4_0_0.AccountBalanceTest + code.api.v4_0_0.AccountTagTest + code.api.v4_0_0.AccountTest + code.api.v4_0_0.ApiCollectionEndpointTest + code.api.v4_0_0.ApiCollectionTest + code.api.v4_0_0.AtmsTest + code.api.v4_0_0.AttributeDefinitionTransactionRequestTest + code.api.v4_0_0.AttributeDefinitionAttributeTest + code.api.v4_0_0.AttributeDefinitionCardTest + code.api.v4_0_0.AttributeDefinitionCustomerTest + code.api.v4_0_0.AttributeDefinitionProductTest + code.api.v4_0_0.AttributeDefinitionTransactionTest + code.api.v4_0_0.AuthenticationTypeValidationTest + code.api.v4_0_0.BankAttributeTests + code.api.v4_0_0.BankTests + code.api.v4_0_0.ConnectorMethodTest + code.api.v4_0_0.ConsentTests + code.api.v4_0_0.CorrelatedUserInfoTest + code.api.v4_0_0.CounterpartyTest + code.api.v4_0_0.CustomerAttributesTest + code.api.v4_0_0.CustomerMessageTest + code.api.v4_0_0.CustomerTest + code.api.v4_0_0.DeleteAccountCascadeTest + code.api.v4_0_0.DeleteBankCascadeTest + code.api.v4_0_0.DeleteCustomerCascadeTest + code.api.v4_0_0.DeleteProductCascadeTest + code.api.v4_0_0.DeleteTransactionCascadeTest + code.api.v4_0_0.DirectDebitTest + code.api.v4_0_0.DoubleEntryTransactionTest + code.api.v4_0_0.EndpointMappingBankLevelTest + code.api.v4_0_0.EndpointMappingTest + code.api.v4_0_0.EndpointTagTest + code.api.v4_0_0.EntitlementTests + code.api.v4_0_0.FirehoseTest + code.api.v4_0_0.ForceErrorValidationTest + code.api.v4_0_0.GetScannedApiVersionsTest + code.api.v4_0_0.JsonSchemaValidationTest + code.api.v4_0_0.LockUserTest + code.api.v4_0_0.MakerCheckerTransactionRequestTest + code.api.v4_0_0.MapperDatabaseInfoTest + code.api.v4_0_0.MySpaceTest + code.api.v4_0_0.OPTIONSTest + code.api.v4_0_0.PasswordRecoverTest + code.api.v4_0_0.ProductFeeTest + code.api.v4_0_0.ProductTest + code.api.v4_0_0.RateLimitingTest + code.api.v4_0_0.ScopesTest + code.api.v4_0_0.SettlementAccountTest + code.api.v4_0_0.StandingOrderTest + code.api.v4_0_0.TransactionAttributesTest + code.api.v4_0_0.TransactionRequestAttributesTest + code.api.v4_0_0.TransactionRequestsTest + code.api.v4_0_0.UserAttributesTest + code.api.v4_0_0.UserCustomerLinkTest + code.api.v4_0_0.UserInvitationApiTest + code.api.v4_0_0.UserTest + code.api.v4_0_0.WebhooksTest - shard: 2 name: "v1_2_1 only (largest unsplittable suite, isolated)" # API1_2_1Test is a single 6604-line suite (~333 scenarios, ~281s). Isolated @@ -102,12 +169,11 @@ jobs: test_filter: >- code.api.v1_2_1 - shard: 3 - name: "v6 + v2_x" + name: "v6 only" + # v6_0_0 isolated: previously bundled with v2_x causing 700s+ runs; + # v2_x moved to shard 7 which had headroom. test_filter: >- code.api.v6_0_0 - code.api.v2_1_0 - code.api.v2_2_0 - code.api.v2_0_0 - shard: 4 name: "v5_1 + v5_0 + v3_0" test_filter: >- @@ -128,7 +194,8 @@ jobs: code.api.http4sbridge code.api.UKOpenBanking - shard: 7 - name: "model + views + customer + util + small data + berlin" + name: "model + views + customer + util + small data + berlin + v2_x" + # v2_0_0/v2_1_0/v2_2_0 moved here from shard 3 to rebalance after v6_0_0 was isolated. test_filter: >- code.model code.views @@ -142,9 +209,12 @@ jobs: code.crm code.accountHolder code.api.berlin + code.api.v2_1_0 + code.api.v2_2_0 + code.api.v2_0_0 - shard: 8 name: "connector + auth + login + mgmt + metrics + remaining (catch-all)" - # catch-all shard: appends any test package not assigned to shards 1-7 + # catch-all shard: appends any test package not assigned to shards 1-7 and 9 # Root-level code.api tests use class-name prefix matching (lowercase classes). # NOTE: classes that sit DIRECTLY in package code.api must be listed here by # FQN-prefix — the catch-all marks the parent package code.api as "covered" once @@ -165,6 +235,15 @@ jobs: code.container code.management code.metrics + code.concurrency + - shard: 9 + name: "v4 Dynamic tests" + # v4_0_0 Dynamic* split: 6 heavy test classes (DynamicEndpointHelperTest 4206 lines, + # DynamicEndpointsTest 2548, DynamicEntityTest 1974, plus 3 smaller ones). + # Prefix code.api.v4_0_0.Dynamic matches all 6 classes; shard 1 lists + # non-Dynamic classes explicitly so no test runs in both shards. + test_filter: >- + code.api.v4_0_0.Dynamic services: redis: @@ -180,11 +259,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 11 + - name: Set up JDK 25 uses: actions/setup-java@v4 with: - java-version: "11" - distribution: "adopt" + java-version: "25" + distribution: "temurin" cache: maven - name: Download compiled output @@ -258,6 +337,12 @@ jobs: echo consents.allowed=true >> obp-api/src/main/resources/props/test.default.props echo hikari.maximumPoolSize=20 >> obp-api/src/main/resources/props/test.default.props echo write_metrics=false >> obp-api/src/main/resources/props/test.default.props + # Log emails instead of opening a real SMTP socket: without this, + # LocalMappedConnector.sendCustomerNotification's EMAIL branch calls + # CommonsEmailWrapper.sendTextEmail which throws ConnectException because + # there's no mail server in CI. That surfaces as 500 in any test that + # hits an endpoint triggering the notification (v5 consent flows, etc.). + echo mail.test.mode=true >> obp-api/src/main/resources/props/test.default.props # Permissions granted to runtime-compiled dynamic-endpoint code inside the security sandbox # (mirrors default.props / production.default.props). Required so dynamic resource-doc bodies # can do JSON extraction (reflection) and read OBP props (getenv); without it the sandbox @@ -271,18 +356,20 @@ jobs: FILTER=$(echo "${{ matrix.test_filter }}" | tr ' ' ',') # Shard 8 is the catch-all: append any test package not explicitly - # assigned to shards 1–7, so new packages are never silently skipped. + # assigned to shards 1–7 and 9, so new packages are never silently skipped. if [ "${{ matrix.shard }}" = "8" ]; then SHARD1="code.api.v4_0_0" SHARD2="code.api.v1_2_1" - SHARD3="code.api.v6_0_0 code.api.v2_1_0 code.api.v2_2_0 code.api.v2_0_0" + SHARD3="code.api.v6_0_0" SHARD4="code.api.v5_1_0 code.api.v5_0_0 code.api.v3_0_0" SHARD5="code.api.ResourceDocs1_4_0 code.api.v3_1_0 code.api.v1_4_0 code.api.v1_3_0" SHARD6="code.api.v7_0_0 code.api.http4sbridge code.api.UKOpenBanking" SHARD7="code.model code.views code.customer code.usercustomerlinks \ code.api.util code.errormessages code.atms code.branches \ - code.products code.crm code.accountHolder code.api.berlin" - ASSIGNED="$SHARD1 $SHARD2 $SHARD3 $SHARD4 $SHARD5 $SHARD6 $SHARD7 ${{ matrix.test_filter }}" + code.products code.crm code.accountHolder code.api.berlin \ + code.api.v2_1_0 code.api.v2_2_0 code.api.v2_0_0" + SHARD9="code.api.v4_0_0.Dynamic" + ASSIGNED="$SHARD1 $SHARD2 $SHARD3 $SHARD4 $SHARD5 $SHARD6 $SHARD7 $SHARD9 ${{ matrix.test_filter }}" # Discover all packages that contain at least one .scala test file ALL_PKGS=$(find obp-api/src/test/scala obp-commons/src/test/scala \ @@ -315,10 +402,27 @@ jobs: # -pl obp-commons,obp-api: obp-commons' own 5 util suites run on whichever # shard's filter matches com.openbankproject.* (the catch-all shard); on every # other shard the filter matches nothing in obp-commons → 0 tests there. + # timeout 1500: hard-kill after 25 min to prevent Pekko non-daemon threads + # (ConsentScheduler etc.) from keeping the JVM alive after tests complete. + # Exit code 124 (timeout) is treated as success — tests are done, JVM just hung. + # set +e: GitHub Actions uses -eo pipefail by default; without it, a 124 exit from + # timeout would abort the step before the rc check below can run. + set +e MAVEN_OPTS="-Xmx3G -Xss2m -XX:MaxMetaspaceSize=1G" \ - mvn process-resources scalatest:test -pl obp-commons,obp-api -DfailIfNoTests=false \ + timeout 1500 mvn process-resources scalatest:test -pl obp-commons,obp-api -DfailIfNoTests=false \ -DwildcardSuites="$FILTER" \ > maven-build-shard${{ matrix.shard }}.log 2>&1 + rc=$? + set -e + # timeout returns 124 when the JVM was killed. That is only benign when the tests had + # successfully finished but Pekko non-daemon threads kept the JVM alive. We must + # require proof from the log instead of blindly converting 124 to success. + if [ $rc -eq 124 ]; then + if grep -q "BUILD SUCCESS" maven-build-shard${{ matrix.shard }}.log; then + rc=0 + fi + fi + exit $rc - name: Report failing tests — shard ${{ matrix.shard }} if: always() diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 0dbd9c28bf..fb2ca1f697 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -14,8 +14,8 @@ env: # # Wall-clock target: # compile ~10 min (parallel with setup of test shards) -# tests ~8 min (3 shards in parallel after compile finishes) -# total ~18 min (vs ~27 min single-job) +# tests ~8 min (8 shards in parallel after compile finishes) +# total ~18 min (vs ~40+ min single-job) # --------------------------------------------------------------------------- jobs: @@ -28,11 +28,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 11 + - name: Set up JDK 25 uses: actions/setup-java@v4 with: - java-version: "11" - distribution: "adopt" + java-version: "25" + distribution: "temurin" cache: maven # caches ~/.m2/repository keyed on pom.xml hash - name: Setup production props @@ -73,26 +73,90 @@ jobs: path: pull/ # -------------------------------------------------------------------------- - # Job 2: test (4-way matrix) + # Job 2: test (9-way matrix) # - # Shard assignment (based on actual clean-run timings): - # Shard 1 ~258s v4_0_0(258) - # Shard 2 ~267s v6_0_0(122) v5_0_0(42) v3_0_0(39) v2_1_0(35) v2_2_0(12) … - # Shard 3 ~252s v1_2_1(137) ResourceDocs(67) berlin(34) util(12) … - # Shard 4 ~232s v5_1_0(79) v3_1_0(65) http4sbridge(52) v7_0_0(45) … + catch-all + # Shard assignment (based on actual clean-run timings on ubuntu-latest 2-core): + # Shard 1 ~300s v4_0_0 non-Dynamic (58 classes; Dynamic* split to shard 9) + # Shard 2 ~281s v1_2_1 (largest single suite — isolated) + # Shard 3 ~250s v6_0_0 (split from v2_x; v2_x moved to shard 7) + # Shard 4 ~232s v5_1_0 + v5_0_0 + v3_0_0 + # Shard 5 ~252s ResourceDocs + v3_1_0 + v1_4_0 + v1_3_0 + # Shard 6 ~200s v7_0_0 + http4sbridge + UKOpenBanking + # Shard 7 ~280s model + views + customer + util + berlin + small data + v2_x + # Shard 8 ~240s connector + auth + login + mgmt + metrics + catch-all + # Shard 9 ~300s v4_0_0 Dynamic* (6 classes: 9 400+ lines each) # -------------------------------------------------------------------------- test: needs: compile runs-on: ubuntu-latest + timeout-minutes: 35 strategy: fail-fast: false matrix: include: - shard: 1 - name: "v4 only (bottleneck pkg)" - # ~258s — single largest package, kept on its own shard + name: "v4 non-Dynamic" + # v4_0_0 split: non-Dynamic classes only (~58 classes). Dynamic* on shard 9. + # Listed by FQN so wildcardSuites doesn't also match Dynamic* classes. test_filter: >- - code.api.v4_0_0 + code.api.v4_0_0.AccountAccessTest + code.api.v4_0_0.AccountBalanceTest + code.api.v4_0_0.AccountTagTest + code.api.v4_0_0.AccountTest + code.api.v4_0_0.ApiCollectionEndpointTest + code.api.v4_0_0.ApiCollectionTest + code.api.v4_0_0.AtmsTest + code.api.v4_0_0.AttributeDefinitionTransactionRequestTest + code.api.v4_0_0.AttributeDefinitionAttributeTest + code.api.v4_0_0.AttributeDefinitionCardTest + code.api.v4_0_0.AttributeDefinitionCustomerTest + code.api.v4_0_0.AttributeDefinitionProductTest + code.api.v4_0_0.AttributeDefinitionTransactionTest + code.api.v4_0_0.AuthenticationTypeValidationTest + code.api.v4_0_0.BankAttributeTests + code.api.v4_0_0.BankTests + code.api.v4_0_0.ConnectorMethodTest + code.api.v4_0_0.ConsentTests + code.api.v4_0_0.CorrelatedUserInfoTest + code.api.v4_0_0.CounterpartyTest + code.api.v4_0_0.CustomerAttributesTest + code.api.v4_0_0.CustomerMessageTest + code.api.v4_0_0.CustomerTest + code.api.v4_0_0.DeleteAccountCascadeTest + code.api.v4_0_0.DeleteBankCascadeTest + code.api.v4_0_0.DeleteCustomerCascadeTest + code.api.v4_0_0.DeleteProductCascadeTest + code.api.v4_0_0.DeleteTransactionCascadeTest + code.api.v4_0_0.DirectDebitTest + code.api.v4_0_0.DoubleEntryTransactionTest + code.api.v4_0_0.EndpointMappingBankLevelTest + code.api.v4_0_0.EndpointMappingTest + code.api.v4_0_0.EndpointTagTest + code.api.v4_0_0.EntitlementTests + code.api.v4_0_0.FirehoseTest + code.api.v4_0_0.ForceErrorValidationTest + code.api.v4_0_0.GetScannedApiVersionsTest + code.api.v4_0_0.JsonSchemaValidationTest + code.api.v4_0_0.LockUserTest + code.api.v4_0_0.MakerCheckerTransactionRequestTest + code.api.v4_0_0.MapperDatabaseInfoTest + code.api.v4_0_0.MySpaceTest + code.api.v4_0_0.OPTIONSTest + code.api.v4_0_0.PasswordRecoverTest + code.api.v4_0_0.ProductFeeTest + code.api.v4_0_0.ProductTest + code.api.v4_0_0.RateLimitingTest + code.api.v4_0_0.ScopesTest + code.api.v4_0_0.SettlementAccountTest + code.api.v4_0_0.StandingOrderTest + code.api.v4_0_0.TransactionAttributesTest + code.api.v4_0_0.TransactionRequestAttributesTest + code.api.v4_0_0.TransactionRequestsTest + code.api.v4_0_0.UserAttributesTest + code.api.v4_0_0.UserCustomerLinkTest + code.api.v4_0_0.UserInvitationApiTest + code.api.v4_0_0.UserTest + code.api.v4_0_0.WebhooksTest - shard: 2 name: "v1_2_1 only (largest unsplittable suite, isolated)" # API1_2_1Test is a single 6604-line suite (~333 scenarios, ~281s). Isolated @@ -101,12 +165,11 @@ jobs: test_filter: >- code.api.v1_2_1 - shard: 3 - name: "v6 + v2_x" + name: "v6 only" + # v6_0_0 isolated: 37 files / ~366 scenarios. Previously bundled with v2_x + # causing 700s+ runs; v2_x moved to shard 7 which had headroom (~98s). test_filter: >- code.api.v6_0_0 - code.api.v2_1_0 - code.api.v2_2_0 - code.api.v2_0_0 - shard: 4 name: "v5_1 + v5_0 + v3_0" test_filter: >- @@ -127,7 +190,8 @@ jobs: code.api.http4sbridge code.api.UKOpenBanking - shard: 7 - name: "model + views + customer + util + small data + berlin" + name: "model + views + customer + util + small data + berlin + v2_x" + # v2_0_0/v2_1_0/v2_2_0 moved here from shard 3 to rebalance after v6_0_0 was isolated. test_filter: >- code.model code.views @@ -141,6 +205,9 @@ jobs: code.crm code.accountHolder code.api.berlin + code.api.v2_1_0 + code.api.v2_2_0 + code.api.v2_0_0 - shard: 8 name: "connector + auth + login + mgmt + metrics + remaining (catch-all)" # catch-all shard: appends any test package not assigned to shards 1-7 @@ -164,6 +231,15 @@ jobs: code.container code.management code.metrics + code.concurrency + - shard: 9 + name: "v4 Dynamic tests" + # v4_0_0 Dynamic* split: 6 heavy test classes (DynamicEndpointHelperTest 4206 lines, + # DynamicEndpointsTest 2548, DynamicEntityTest 1974, plus 3 smaller ones). + # Prefix code.api.v4_0_0.Dynamic matches all 6 classes; shard 1 lists + # non-Dynamic classes explicitly so no test runs in both shards. + test_filter: >- + code.api.v4_0_0.Dynamic services: redis: @@ -179,11 +255,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 11 + - name: Set up JDK 25 uses: actions/setup-java@v4 with: - java-version: "11" - distribution: "adopt" + java-version: "25" + distribution: "temurin" cache: maven - name: Download compiled output @@ -276,17 +352,21 @@ jobs: FILTER=$(echo "${{ matrix.test_filter }}" | tr ' ' ',') # Shard 8 is the catch-all: append any test package not explicitly - # assigned to shards 1–7, so new packages are never silently skipped. + # assigned to shards 1–7 and 9, so new packages are never silently skipped. if [ "${{ matrix.shard }}" = "8" ]; then + # Shard 1 lists v4 non-Dynamic classes explicitly; shard 9 covers Dynamic*. + # Use code.api.v4_0_0 as the assigned prefix so the catch-all treats the + # whole v4_0_0 package as covered (prevents Dynamic* from being re-added). SHARD1="code.api.v4_0_0" SHARD2="code.api.v1_2_1" - SHARD3="code.api.v6_0_0 code.api.v2_1_0 code.api.v2_2_0 code.api.v2_0_0" + SHARD3="code.api.v6_0_0" SHARD4="code.api.v5_1_0 code.api.v5_0_0 code.api.v3_0_0" SHARD5="code.api.ResourceDocs1_4_0 code.api.v3_1_0 code.api.v1_4_0 code.api.v1_3_0" SHARD6="code.api.v7_0_0 code.api.http4sbridge code.api.UKOpenBanking" SHARD7="code.model code.views code.customer code.usercustomerlinks \ code.api.util code.errormessages code.atms code.branches \ - code.products code.crm code.accountHolder code.api.berlin" + code.products code.crm code.accountHolder code.api.berlin \ + code.api.v2_1_0 code.api.v2_2_0 code.api.v2_0_0" ASSIGNED="$SHARD1 $SHARD2 $SHARD3 $SHARD4 $SHARD5 $SHARD6 $SHARD7 ${{ matrix.test_filter }}" # Discover all packages that contain at least one .scala test file @@ -320,10 +400,27 @@ jobs: # -pl obp-commons,obp-api: obp-commons' own 5 util suites run on whichever # shard's filter matches com.openbankproject.* (the catch-all shard); on every # other shard the filter matches nothing in obp-commons → 0 tests there. + # timeout 1500: hard-kill after 25 min to prevent Pekko non-daemon threads + # (ConsentScheduler etc.) from keeping the JVM alive after tests complete. + # Exit code 124 (timeout) is treated as success — tests are done, JVM just hung. + # set +e: GitHub Actions uses -eo pipefail by default; without it, a 124 exit from + # timeout would abort the step before the rc check below can run. + set +e MAVEN_OPTS="-Xmx3G -Xss2m -XX:MaxMetaspaceSize=1G" \ - mvn process-resources scalatest:test -pl obp-commons,obp-api -DfailIfNoTests=false \ + timeout 1500 mvn process-resources scalatest:test -pl obp-commons,obp-api -DfailIfNoTests=false \ -DwildcardSuites="$FILTER" \ > maven-build-shard${{ matrix.shard }}.log 2>&1 + rc=$? + set -e + # timeout returns 124 when the JVM was killed. That is only benign when the tests had + # successfully finished but Pekko non-daemon threads kept the JVM alive. We must + # require proof from the log instead of blindly converting 124 to success. + if [ $rc -eq 124 ]; then + if grep -q "BUILD SUCCESS" maven-build-shard${{ matrix.shard }}.log; then + rc=0 + fi + fi + exit $rc - name: Report failing tests — shard ${{ matrix.shard }} if: always() diff --git a/.metals-config.json b/.metals-config.json deleted file mode 100644 index ed54fa6477..0000000000 --- a/.metals-config.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "maven": { - "enabled": true - }, - "metals": { - "serverVersion": "1.0.0", - "javaHome": "/usr/lib/jvm/java-17-openjdk-amd64", - "bloopVersion": "2.0.0", - "superMethodLensesEnabled": true, - "enableSemanticHighlighting": true, - "compileOnSave": true, - "testUserInterface": "Code Lenses", - "inlayHints": { - "enabled": true, - "hintsInPatternMatch": { - "enabled": true - }, - "implicitArguments": { - "enabled": true - }, - "implicitConversions": { - "enabled": true - }, - "inferredTypes": { - "enabled": true - }, - "typeParameters": { - "enabled": true - } - } - }, - "buildTargets": [ - { - "id": "obp-commons", - "displayName": "obp-commons", - "baseDirectory": "file:///home/marko/Tesobe/GitHub/constantine2nd/OBP-API/obp-commons/", - "tags": ["library"], - "languageIds": ["scala", "java"], - "dependencies": [], - "capabilities": { - "canCompile": true, - "canTest": true, - "canRun": false, - "canDebug": true - }, - "dataKind": "scala", - "data": { - "scalaOrganization": "org.scala-lang", - "scalaVersion": "2.12.20", - "scalaBinaryVersion": "2.12", - "platform": "jvm" - } - }, - { - "id": "obp-api", - "displayName": "obp-api", - "baseDirectory": "file:///home/marko/Tesobe/GitHub/constantine2nd/OBP-API/obp-api/", - "tags": ["application"], - "languageIds": ["scala", "java"], - "dependencies": ["obp-commons"], - "capabilities": { - "canCompile": true, - "canTest": true, - "canRun": true, - "canDebug": true - }, - "dataKind": "scala", - "data": { - "scalaOrganization": "org.scala-lang", - "scalaVersion": "2.12.20", - "scalaBinaryVersion": "2.12", - "platform": "jvm" - } - } - ] -} diff --git a/.sdkmanrc b/.sdkmanrc index dac70193f8..b5e2fc1053 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=11.0.28-tem +java=25.0.3-tem diff --git a/LIFT_HTTP4S_MIGRATION.md b/LIFT_HTTP4S_MIGRATION.md deleted file mode 100644 index 3e22f1c185..0000000000 --- a/LIFT_HTTP4S_MIGRATION.md +++ /dev/null @@ -1,325 +0,0 @@ -# Lift → http4s Migration — COMPLETE - -## Status - -**The Lift → http4s migration of the HTTP request path is complete.** Every OBP API endpoint is served by native http4s `HttpRoutes[IO]`: - -- All version files **v1.2.1 → v7.0.0** -- **Berlin Group** v1.3 + v2 -- **UK Open Banking** v2.0 + v3.1 -- **Dynamic Entity / Dynamic Endpoint** runtime dispatch -- **Resource-docs / message-docs / openapi.yaml** (centralized `Http4sResourceDocs`) -- **Auth handlers**: DirectLogin, OpenID Connect, AliveCheck - -`Http4sLiftWebBridge` has been **deleted**; `lift-webkit` has been **removed from `pom.xml`**. There is no Lift fallback in the request path — any unmatched `/obp/*` path returns a JSON 404 from `notFoundCatchAll`. The **"Lift Web removed"** milestone is therefore achieved. - -The remaining Lift dependencies are the **non-web libraries** — `lift-mapper` (ORM / database layer), plus `lift-json` / `lift-common` / `lift-util` — kept deliberately. Replacing `lift-mapper` is a separate long-term effort tracked under [What remains](#what-remains--lift-mapper). - ---- - -## Principle - -API version numbers reflect **API contract changes** (new/changed fields, new behaviour). The underlying framework is invisible to clients. Lift → http4s was a refactoring: it happened **in-place** inside the existing version file at the existing URL. No version bump. - -A new version (e.g. v7.0.0) is used only when the API contract itself changes — new fields, changed request/response shape, new behaviour. - ---- - -## Current Architecture - -OBP-API runs as a **single http4s Ember server** (single process, single port). The application entry point is a Cats Effect `IOApp` (`Http4sServer`). Lift is no longer an HTTP server — Jetty, the servlet container, and the request bridge have all been removed. - -Lift now plays exactly one role: - -- **`lift-mapper` ORM / Database** — Mapper manages schema creation, migrations, and all data access (`MappedBank`, `AuthUser`, etc.). A handful of `net.liftweb.json` / `net.liftweb.common` (`Box`/`Full`/`Empty`) serialisation helpers are also still used; these are library utilities, not the Lift web stack. - -### Entry point — `Http4sServer.scala` - -`Http4sServer` extends `IOApp`. On startup it: - -1. Calls `bootstrap.liftweb.Boot().boot()` to initialise Lift Mapper, connectors, and OBP configuration (DB/ORM init only — no `LiftRules` request-path registrations remain active). -2. Parses the configured `hostname` and `dev.port` props (defaults: `127.0.0.1`, `8080`). -3. Starts an Ember server with the application defined in `Http4sApp.httpApp`. - -### Priority routing - -Routes are tried in order (see `Http4sApp.baseServices`): `corsHandler` (OPTIONS) → `AppsPage` → `StatusPage` → `Http4sResourceDocs` → `Http4s510` → `Http4s600` → `Http4s500` → `Http4s700` → `Http4sBGv2` → `Http4sUKOBv200` → `Http4sUKOBv310` → `Http4sBGv13` (+`Http4sBGv13Alias`) → `Http4s400` → `Http4s310` → `Http4s300` → `Http4s220` → `Http4s210` → `Http4s200` → `Http4s140` → `Http4s130` → `Http4s121` → `dynamicEntityRoutes` → `dynamicEndpointRoutes` → `DirectLoginRoutes` → `Http4sOpenIdConnect` → `AliveCheckRoutes` → `notFoundCatchAll` (JSON 404). - -There is **no Lift fallback** — the chain terminates in `notFoundCatchAll`, which returns a JSON 404 for any unmatched path. The non-numeric ordering (v510 before v600, v500 after v600, etc.) doesn't affect correctness because each per-version service gates on its own version prefix; ordering only matters when two services overlap on the same URL pattern. - -``` -HTTP Request - │ - ▼ -Http4sServer (IOApp / Ember) - │ - ▼ -corsHandler → AppsPage → StatusPage → Http4sResourceDocs - → Http4s510 → Http4s600 → Http4s500 → Http4s700 - → Http4sBGv2 → Http4sUKOBv200 → Http4sUKOBv310 → Http4sBGv13(+Alias) - → Http4s400 → Http4s310 → Http4s300 → Http4s220 → Http4s210 → Http4s200 - → Http4s140 → Http4s130 → Http4s121 - → dynamicEntityRoutes → dynamicEndpointRoutes - → DirectLoginRoutes → Http4sOpenIdConnect → AliveCheckRoutes - → notFoundCatchAll (JSON 404 — no Lift fallback) - │ - ▼ -HTTP Response (with standard headers) -``` - -### Body caching - -http4s request bodies are single-shot streams. The first version's `ResourceDocMiddleware.fromRequest` consumes the body to build the CallContext; any later path-rewriting bridge hop (v400→v310→…→v210) that re-reads `req.bodyText` would get an empty stream and the handler would 500. `Http4sApp.cacheBodyOnce` pre-reads the body and stashes it in `cachedBodyKey`, so every downstream `fromRequest` reads from the attribute instead of the drained stream. GET/DELETE/HEAD/OPTIONS skip this. - -### Version enable/disable semantics - -Two Props govern which API versions are served: `api_disabled_versions` and `api_enabled_versions` (allowlist; empty means "all"). They are enforced **once at startup**, by `Http4sApp.gate`: - -```scala -private def gate(version: ScannedApiVersion, routes: HttpRoutes[IO]): HttpRoutes[IO] = - if (APIUtil.versionIsAllowed(version)) routes else HttpRoutes.empty[IO] -``` - -A disabled version's top-level routes are replaced with `HttpRoutes.empty[IO]`, so a direct `GET /obp/vX.Y.Z/...` falls through the chain to `notFoundCatchAll` (JSON 404). - -**Cascade is intentionally unaffected.** Each `Http4sXxx` has a path-rewriting bridge to the next-lower version that calls `code.api.vN.HttpNxx.wrappedRoutesVNxxServices` *directly*, bypassing `Http4sApp.gate`. `ResourceDocMiddleware` does **not** re-check `implementedInApiVersion` per request either (`ResourceDocMiddleware.isEndpointEnabled` deliberately has no `versionAllowed` parameter — `ResourceDocMiddlewareEnableDisableTest` pins this). So an endpoint originally declared in v2.0.0 stays reachable via `/obp/v4.0.0/...` even when v2.0.0 is disabled, as long as v4.0.0 is enabled. - -This preserves the documented OBP-API contract: newer versions act as the supported entry point for older endpoints' functionality. Operators can retire a version's *URL prefix* with `api_disabled_versions` without losing the underlying endpoints from newer prefixes. To retire a specific endpoint everywhere, use `api_disabled_endpoints` (operationId list) — that **is** enforced per request by the middleware and so kills the endpoint on every prefix it would otherwise be reachable from. - -A brief regression in early 2026-05 inverted this: a `versionAllowed` check was added inside the middleware, making `api_disabled_versions` kill cascaded reachability too. Restored 2026-05-26. If you're tempted to put the per-request version check back, read the `isEndpointEnabled` docstring first — it spells out the design rationale, and the "version-level gating is delegated to Http4sApp.gate" feature in the unit test will fail loudly. - ---- - -## What "in-place migration" means per file - -### `APIMethods{version}.scala` - -| Before (Lift) | After (http4s) | -|---|---| -| `self: RestHelper =>` on the trait | removed | -| `lazy val xyz: OBPEndpoint` | `val xyz: HttpRoutes[IO]` | -| `case "path" :: Nil JsonGet _` | `case req @ GET -> \`prefixPath\` / "path"` | -| `authenticatedAccess(cc)` in for-comp | pick the right `EndpointHelpers.*` helper | -| `implicit val ec = EndpointContext(Some(cc))` | removed | -| `yield (json, HttpCode.\`200\`(cc))` | `yield json` | -| `ResourceDoc(root, ...)` | `ResourceDoc(implementedInApiVersion, ..., http4sPartialFunction = Some(root))` | - -### `OBPAPI{version}.scala` - -| Before | After | -|---|---| -| `extends OBPRestHelper` | removed | -| `registerRoutes(routes, allResourceDocs, apiPrefix)` | expose `val allRoutes: HttpRoutes[IO]` | -| registered via Boot / LiftRules | wired into `Http4sApp.baseServices` chain | - -See `CLAUDE.md § Migrating a Lift Endpoint to http4s` for the full Rule 1–5 reference. The Lift `APIMethodsXYZ.scala` files are retained as **commented-out source-of-truth** for the ResourceDoc parity audit (see below) and as the frozen STABLE API surface for `FrozenClassTest`; they are comments, not active routes. - ---- - -## What was migrated - -### Per-version files (bottom-up; each has a path-rewriting bridge to the version below) - -| # | File | Own endpoints | http4s file | -|---|---|---|---| -| 1 | `APIMethods121` | 70 | `Http4s121.scala` — all 323 API1_2_1Test scenarios pass | -| 2 | `APIMethods130` | 3 | `Http4s130.scala` — bridge to `Http4s121` | -| 3 | `APIMethods140` | 11 | `Http4s140.scala` — bridge to `Http4s130` | -| 4 | `APIMethods200` | 40 | `Http4s200.scala` — 37 own + bridge to `Http4s140` | -| 5 | `APIMethods210` | 28 | `Http4s210.scala` — 25 own + bridge to `Http4s200`; 79 tests pass | -| 6 | `APIMethods220` | 19 | `Http4s220.scala` — 18 own + bridge to `Http4s210`; 27 tests pass | -| 7 | `APIMethods300` | 47 | `Http4s300.scala` — bridge to `Http4s220`; 86 tests pass | -| 8 | `APIMethods310` | 102 | `Http4s310.scala` — 100 functional + bridge to `Http4s300`; 181 tests pass. `getObpConnectorLoopback` is a native http4s route (returns 400 NotImplemented); `getMessageDocsSwagger` routing is owned by `Http4sResourceDocs` (in-file `HttpRoutes.empty` stub kept only so `nameOf(...)` compiles for `FrozenClassTest`). | -| 9 | `APIMethods400` | 258 | **258 / 258 (100%)**. `Http4s400.scala` — 253 unique handlers + 8 ResourceDoc aliases for transaction-request-type variants (shared `createTransactionRequest` wildcard handler; `literalAllCapsSegments` in `Http4sSupport.scala` dispatches the matcher to the per-type doc). All 35/35 v4-over-older URL+verb overrides migrated (avoids the bridge-cascade hijack). | -| 10 | `APIMethods500` | 10 | `Http4s500.scala` — all v5.0.0 originals | -| 11 | `APIMethods510` | 111 | `Http4s510.scala` — `createConsent` exposed as `createConsentImplicit` (one handler, `scaMethod ∈ {EMAIL, SMS, IMPLICIT}` guard covers all three SCA-method URLs) | -| 12 | `APIMethods600` | 243 (35 overrides + 208 originals) | **243 / 243 (100%)**. `Http4s600.scala` — introduced the **lazy val + helper-def init pattern** to dodge the JVM 64KB `` method-size limit (`val xxx` ⇒ `lazy val xxx`; `resourceDocs += ResourceDoc(...)` grouped into `private def initXxxResourceDocs(): Unit` blocks). All later per-version files adopt this from the start. | - -> **JVM 64KB `` limit**: around the 140-endpoint mark a per-version object's `` hits the JVM method-size limit. The fix (shipped in `Http4s600`/`Http4s400`): `lazy val` endpoints (lambda materialisation moves into per-field `lzycompute`) + `resourceDocs +=` grouped into `initXxx()` helper defs (each with its own 64KB budget). - -### Open-banking standards - -| Standard | Location | Status | -|---|---|---| -| **Berlin Group v1.3** | `code/api/berlin/group/v1_3/Http4sBGv13{,AIS,PIS,PIIS,SigningBaskets,Alias}.scala` — 6 http4s files | ✅ http4s, wired into `Http4sApp` (`Http4sBGv13` + `Http4sBGv13Alias`) | -| **Berlin Group v2** | `code/api/berlin/group/v2/Http4sBGv2.scala` | ✅ http4s | -| **UK Open Banking v2.0.0** | `code/api/UKOpenBanking/v2_0_0/Http4sUKOBv200{,AIS}.scala` | ✅ http4s (`/open-banking/v2.0/*`) | -| **UK Open Banking v3.1.0** | `code/api/UKOpenBanking/v3_1_0/Http4sUKOBv310*.scala` — 21 http4s files | ✅ http4s (`/open-banking/v3.1/*`) | -| Bahrain OBF v1.0.0 | `code/api/BahrainOBF/v1_0_0/*` — 22 files | 🗑 commented-out dead code (whole files `//`-commented in `d19af2b92`, 2026-05-22). No routes, no http4s port. Since the bridge is gone, these are unreachable. | -| AU OpenBanking v1.0.0 | `code/api/AUOpenBanking/v1_0_0/*` — 11 files | 🗑 commented-out dead code (`d19af2b92`) | -| STET v1.4 | `code/api/STET/v1_4/*` — 5 files | 🗑 commented-out dead code (`d19af2b92`) | -| MxOF / CNBV9 v1.0.0 | `code/api/MxOF/*` — 4 files | 🗑 commented-out dead code (`d19af2b92`) | -| Polish v2.1.1.1 | `code/api/Polish/v2_1_1_1/*` — 5 files | 🗑 commented-out dead code (`d19af2b92`) | -| Sandbox | `code/api/sandbox/SandboxApiCalls.scala` | 🗑 commented-out dead code (`7f3c51f5e`) | - -The five retired standards + Sandbox are **commented-out source files with no route registration**. The `code.api.*.ApiCollector` / `OBP_*` `ScannedApis` classes inside them are inert (the code is `//`-commented, so `ClassScanUtils` can't discover them and `APIUtil`'s `allResourceDocs` aggregation no longer references them). A future cleanup PR can delete the files outright, or — if any standard is wanted back — port it to http4s the way BG v1.3 / UK OB were. - -### Auth stack - -| Handler | Path | Status | -|---|---|---| -| `DirectLogin` | `POST /my/logins/direct` | ✅ `code.api.DirectLoginRoutes` serves the bare path (gated on `allow_direct_login`); versioned path served by each `Http4sXxx`. `LiftRules.statelessDispatch.append(DirectLogin)` removed from `Boot.scala`. | -| `OpenIdConnect` | `GET\|POST /auth/openid-connect/callback{,-1,-2}` | ✅ **`code.api.Http4sOpenIdConnect`** — native http4s. **Portal-session decision resolved as fork (a) "drop portal-login":** the success branch no longer calls `AuthUser.logUserIn` / `S.redirectTo`; it issues an OBP DirectLogin token via `DirectLogin.issueTokenForUser(...)` and returns it. The old `openidconnect.scala` is fully commented out. Pure route tests live in `Http4sOpenIdConnectRoutesTest`. | -| `AliveCheck` | `GET /alive` | ✅ `code.api.AliveCheckRoutes`; Lift dispatch removed. | -| `GatewayLogin` | gateway JWT exchange | ✅ Library-only validator (no routes). Vestigial `extends RestHelper` removed. | -| `DAuth` | dAuth JWT exchange | ✅ Library-only validator (no routes). Vestigial `extends RestHelper` removed. | -| `OAuth2` (`OAuth2Login`) | Bearer-token validator | ✅ Library-only (Google / Yahoo / Azure / Keycloak / OBPOIDC / Hydra). Vestigial `extends RestHelper` removed. | -| `OAuth 1.0a` | — | ✅ **Removed entirely** in `51820c75e` (2026-02-20). `oauth1.0.scala` deleted, `OAuthHandshake` unregistered, header detection removed from `OBPRestHelper.scala`. `getConsumerFromDirectLoginToken` / `getUserFromDirectLoginToken` took over consumer/user lookup. | -### ~~Prerequisite~~ Prerequisite (done): aggregation bug fix - -> Kept on purpose: `code/model/OAuth.scala` (backs the general `Consumer` entity used by all auth methods) and `APIUtil.OAuth` (misnamed but live **test** infrastructure — the `<@` signer adds `Authorization: DirectLogin token=...` headers and is imported by hundreds of test files; renaming is a separate cleanup). -~~`V7ResourceDocsAggregationTest` is intentionally failing.~~ **Fixed in `efb97531e` (2026-05-19)** — *"fix(resource-docs): correct v7 aggregation specifiedUrl and remove shadowed v7 handler"*. Two root causes addressed: (1) `ResourceDocs1_4_0` registered the same `(GET, /resource-docs/API_VERSION/obp)` doc twice, so v7 aggregation surfaced a duplicate; (2) `getAllResourceDocsObpCached` cached `specifiedUrl` per dynamic-endpoint doc with `case Some(_) => it`, so the first caller froze the URL and every later request inherited it. `getResourceDocsObpV700` now calls `getResourceDocsList`, which aggregates the full cascade (~949 docs on a live server). The centralized service must preserve this contract — `V7ResourceDocsAggregationTest` now acts as the regression guard. - -### Dynamic dispatch, resource-docs, and singletons - -| Component | Status | -|---|---| -| **DynamicEntity** (`/obp/dynamic-entity/*`) | ✅ `code.api.dynamic.entity.Http4sDynamicEntity` — native http4s, replaces the Lift `OBPAPIDynamicEntity` dispatch. | -| **DynamicEndpoint** (`/obp/dynamic-endpoint/*`) | ✅ `code.api.dynamic.endpoint.Http4sDynamicEndpoint` — fully native (no Lift `Req`, `S.init`, `buildLiftReq`, or `liftResponseToHttp4s`). | -| **Resource-docs** (`/obp/*/resource-docs/{API_VERSION}/{obp,swagger,openapi,openapi.yaml}`) | ✅ Centralized `code.api.util.http4s.Http4sResourceDocs`, matched before any per-version service (version-polymorphic: the `API_VERSION` path segment controls output). Retired 10 `LiftRules.statelessDispatch.append(ResourceDocs140..600)` entries + the raw `openapi.yaml` Lift `serve {...}` block. The `getResourceDocsObpV700` aggregation bug is fixed (`V7ResourceDocsAggregationTest` passes). ResourceDocsTest (63) + SwaggerDocsTest (10) green. | -| **message-docs** (`/obp/*/message-docs/{CONNECTOR}/swagger2.0`) | ✅ `Http4sResourceDocs.handleGetMessageDocsSwagger` via wildcard route. | -| `ImporterAPI` | ✅ **Retired** — legacy `POST /obp_transactions_saver/api/transactions` shared-secret bulk-insert endpoint, its `TransactionInserter` LiftActor, and the connector helpers it relied on all removed. | -| `testResourceDoc` (`APIMethods140` `/dummy`) | ✅ Removed — dev-mode-only stub, no production behaviour. | -Currently served via a raw Lift `serve { case Req(..., "openapi.yaml", ...) }` block that bypasses `registerRoutes` entirely. Needs a dedicated http4s route (no ResourceDocMiddleware) added to the centralized service. - -### Caching - -`Caching.getStaticSwaggerDocCache()` / `setStaticSwaggerDocCache()` are framework-agnostic and already used from within the http4s path. No migration work needed. - -### Steps - -1. ~~Fix aggregation bug in `getResourceDocsObpV700` → make `V7ResourceDocsAggregationTest` pass.~~ **Done** in `efb97531e` (2026-05-19). See the Prerequisite section above. -2. Extract shared handler logic into `Http4sResourceDocs` service; wire into `Http4sApp`. -3. Add `openapi.yaml` route to the same service. -4. ~~Port `getMessageDocsSwagger` from `APIMethods310` into the same service~~ — **Done.** Now served by `Http4sResourceDocs.handleGetMessageDocsSwagger` via the wildcard `/obp/*/message-docs/{CONNECTOR}/swagger2.0` route matched before any per-version service. The `val getMessageDocsSwagger: HttpRoutes[IO] = HttpRoutes.empty` stub in `Http4s310.scala` exists only to satisfy the `FrozenClassTest` API-surface check. -5. Remove resource-docs from the per-version Lift objects (`ResourceDocs140`–`ResourceDocs600`) once the centralized service covers them. - ---- - -## ResourceDoc parity (content workstream — independent of serving) - -This is a **separate workstream** from the serving migration above (which is complete). It covers the **content** of each migrated `ResourceDoc(...)` declaration: the goal is for every http4s `ResourceDoc(...)` to render identically to its Lift original, so the public API docs aren't silently degraded. The figures below are the last recorded audit (2026-05-21) and may have moved since; re-run the audit script for current numbers. - -### Principle - -**`APIMethodsXYZ.scala` (Lift) is the source of truth.** The commented-out Lift ResourceDocs inside each `APIMethodsXYZ.scala` are the canonical reference for what the http4s version should render: URL templates, verb casing, summaries, descriptions, example bodies, error lists, tags. **Do NOT edit those files to make the audit pass** — the audit compares http4s against the Lift source-of-truth. When the audit flags a diff, the resolution is either (a) update http4s to match Lift, or (b) document the difference at the http4s site as a known intentional drift (placeholder rename for middleware, upstream-driven case-class shift, etc.). Rewriting the Lift comments runs the comparison backwards and erases the historical record. (Mistakes in commits `d95c1df01` and `6154bf2cc` did this; reverted in `27f48af72`.) - -**Stub fidelity verified.** Commits `810589330` (v6) and `88f46f854` (v5.1) replaced live Lift code with commented-out stubs: **0 field diffs across 243/243 v6 docs and 111/111 v5.1 docs**. The stubs are an exact preservation of the original Lift ResourceDocs. - -### Tooling (`scripts/`) - -| Script | Role | -|---|---| -| `check_lift_http4s_resource_doc_parity.py` | Read-only audit. Parses both files, matches by `nameOf(...)`, reports per-field diffs. `--field=X`, `--list-only`. | -| `rehydrate_resource_docs.py` | Lifts positional args 7/8/9 (description, exampleRequestBody, successResponseBody) from commented Lift blocks into http4s. `split-init` subcommand for the JVM 64KB workaround. | -| `restore_resource_doc_bodies.py` | Restores any subset of (summary, description, exampleRequestBody, successResponseBody, errorResponseBodies, tags) from Lift into http4s. `--fields=X,Y`, `--only=ep`. | - -### Last recorded drift (audit 2026-05-21) - -| Version | shared | mismatch | only-lift | only-http4s | Status | -|---|---|---|---|---|---| -| v1_2_1 | 70 | 6 | 0 | 0 | semantic fields restored; 6 structural drifts remain | -| v1_3_0 | 3 | 0 | 0 | 0 | clean | -| v1_4_0 | 10 | 1 | 0 | 0 | one minor | -| v2_0_0 | 37 | 1 | 0 | 0 | 1 structural drift remains | -| v2_1_0 | 23 | 1 | 5 | 2 | 1 structural drift remains | -| v2_2_0 | 18 | 0 | 0 | 18 | Lift trait fully retired upstream (`71892f5cb`); audited via git history; 3 middleware URL renames remain | -| v3_0_0 | 47 | 4 | 0 | 0 | 4 middleware-driven URL renames remain | -| v3_1_0 | 102 | 5 | 0 | 0 | 5 placeholder renames remain | -| v4_0_0 | 254 | 20 | 2 | 5 | 20 structural drifts (placeholder renames + 1 verb fix) remain | -| v5_0_0 | 39 | 8 | 0 | 3 | descriptions restored; structural/errors remain | -| v5_1_0 | 111 | 1 | 1 | 2 | one verb-casing drift | -| v6_0_0 | 243 | 12 | 0 | 1 | 11 placeholder renames + 1 routing-shape upstream change | -| **Total** | **956** | **60** | | | | - -The per-version drift breakdowns (v6 COUNTERPARTY_ID renames, v4 GRANT_VIEW_ID / DYNAMIC_RESOURCE_DOC_ID, v3 firehose `FIREHOSE_*` renames, v5 system-view error-accuracy improvements, etc.) are middleware-driven placeholder renames or deliberate http4s improvements. The two only-lift v4 endpoints (`getAllAuthenticationTypeValidationsPublic`, `getAllJsonSchemaValidationsPublic`) are a known **migration gap** — port them or confirm they're intentionally dropped. - -### Strategy for each remaining drift - -1. **Default**: fix http4s to match Lift verbatim (`restore_resource_doc_bodies.py`). -2. **Documented exception**: where the drift is a deliberate http4s improvement or required by middleware semantics, leave it and add a `// Lift had X; we use Y because Z` comment at the http4s ResourceDoc site. -3. **Never**: edit `APIMethodsXYZ.scala` to make the audit pass — the Lift comments are the canonical record. - -Reserved ALL_CAPS placeholders in middleware (`BANK_ID`, `ACCOUNT_ID`, `VIEW_ID`, `COUNTERPARTY_ID`) plus the literal SCA/transaction-type segments in `literalAllCapsSegments` drive most renames: when an endpoint needs a same-shape var without middleware lookup, it's renamed to a non-reserved variant (e.g. `COUNTERPARTY_ID_PARAM`, `NEW_ACCOUNT_ID`, `FIREHOSE_BANK_ID`) in **both** the http4s and Lift ResourceDocs. - ---- - -## Lift Web teardown — completed - -The full "remove Lift Web" milestone is done. For the record, what landed: - -1. **`Http4sLiftWebBridge` deleted** — there is no request bridge; the chain ends in `notFoundCatchAll`. The bridge-traffic audit instrumentation (`Http4sLiftBridgeTraffic`, `GET /admin/lift-bridge-traffic`) that was used to prove `real_work[]` had drained is gone with it. -2. **`lift-webkit` removed from `pom.xml`** — the Lift web library is no longer a dependency. -3. **`Boot.scala` request-path hooks removed** — all `LiftRules.statelessDispatch.append(...)` (DirectLogin, ResourceDocs140–600, aliveCheck), `LiftRules.dispatch.append(OpenIdConnect)`, `addToPackages`, `exceptionHandler`/`uriNotFound`/`early`/`supplementalHeaders` request-path hooks are gone. Boot now does ORM init + connector/config setup + the Mapper schemifier + shutdown hooks only. -4. **OpenID Connect migrated** (fork a — drop portal-login). The one hard Lift-Web dependency in the request path (`AuthUser.logUserIn` / `S.redirectTo` seeding a Lift `SessionVar` portal session) was resolved by issuing a DirectLogin token instead. -5. **0 `net.liftweb.http` references anywhere in `obp-api/src`** — code, dead import comments, and doc-strings all removed. The previously-vestigial `net.liftweb.http.S.redirectTo(homePage)` in `AuthUser.logout` (dead code, never called) has been deleted, together with the 91 commented-out `//import net.liftweb.http...` lines. The only `net.liftweb.http.*` left in the running system is the inherited, unreachable `MegaProtoUser.logout` inside the Lift library jar — not OBP source. - -> `APIUtil.SS.init(...)` wrappers (e.g. in `Http4s400.scala`) are **not** Lift-Web code — `SS` is a thread-local that the `lift-mapper`-based `LocalMappedConnectorInternal` reads (`SS.user`). It's a legitimate adapter for the ORM layer, which stays until lift-mapper is replaced. - -> **Known gap — connector-export endpoint not migrated.** The prop-gated -> `connector.name.export.as.endpoints` feature was removed with the Lift teardown and -> **not** ported to http4s. At `d5f8716`, `Boot.scala` conditionally called -> `ConnectorEndpoints.registerConnectorEndpoints`, which served `/connector/{methodName}` -> via Lift `oauthServe` (role-gated by `canGetConnectorEndpoint`, reflectively invoking the -> active connector's methods), plus a startup `assert` validating the prop value. That Lift -> endpoint + the Boot registration + the validation are gone; there is no http4s replacement -> (the http4s `/connector/loopback` and `/management/connector/metrics` are different -> endpoints). It is off by default — deployments that set the prop silently lose both the -> endpoint and the startup validation. Recorded here so it isn't lost; migrate to an -> `/obp/.../connector/...` route only if a deployment actually needs it. - ---- - -## What remains — `lift-mapper` - -**Out of scope for this migration.** `net.liftweb.mapper.*` is still the ORM across the codebase (100+ files): `AuthUser extends MegaProtoUser`, `Schemifier.schemify` in `Boot.scala`, all `MappedXxx` entities. Replacing it (with Doobie / Slick or similar) is a separate multi-month effort. - -**"Lift Web removed" ≠ "Lift removed."** - -- *Lift Web removed* (✅ **done**) — the HTTP request path no longer touches Lift: `lift-webkit` out of `pom.xml`, `Http4sLiftWebBridge` deleted, `Boot.scala` request-path hooks gone. `lift-mapper` is still the ORM. -- *Lift removed* (not done) — `net.liftweb:*` fully out of the dependency graph; requires the lift-mapper replacement above. - -Decide which bar a release is hitting before announcing it; conflating them invites either an overstatement or an avoidable months-long delay. - ---- - -## Reusable lessons - -1. **JVM 64KB `` limit** — adopt `lazy val xxx: HttpRoutes[IO] = ...` + `private def initXxxResourceDocs(): Unit` blocks in every per-version file from the start; don't wait until you hit the wall. -2. **`S.request`-bound Lift handlers** need an http4s-friendly entry point that accepts pre-parsed parameters. DirectLogin's `createTokenFuture` ignored its argument and re-read from `S.request` via `getAllParameters`; the fix threaded params through `validatorFutureWithParams`. Audit any handler for `S.request`/`S.param`/`S.queryString` reads before designing its http4s entry point. -3. **`Future.failed(new Exception)` produces 500** — use `unboxFullOrFail(Empty, ..., 400)` or `NewStyle.function.tryons(msg, 400, ...)` to return the intended 4xx. -4. **`isStatisticallyTooPermissive` is sample-pool-dependent** — locally, a fresh test DB with a single user causes spurious ABAC rejections. Seed enough users. -5. **Reserved ALL_CAPS placeholders** in middleware — when an endpoint needs a same-shape var without middleware lookup, rename to a non-reserved variant (e.g. `COUNTERPARTY_ID_PARAM`) in both the http4s and Lift ResourceDocs. -6. **Bridge-cascade hijack** — when a new version overrides an older URL+verb, migrate the override into the new version's own routes *before* wiring it into the chain, or the request cascades down the path-rewriting bridges to the older handler. (Now that the chain ends in `notFoundCatchAll`, an un-migrated override cascades to an older http4s handler or 404s — there is no Lift safety net.) - ---- - -## Why http4s? - -- **Non-blocking I/O** — small fixed thread pool (CPU cores), fibres suspend on I/O. Thousands of concurrent requests without thread-pool tuning. -- **Lower memory** — no thread-per-request overhead. -- **Modern Scala ecosystem** — first-class Cats Effect, fs2 streaming, functional patterns. -- **No servlet container** — Jetty and WAR packaging gone entirely. - ---- - -## Running - -```sh -MAVEN_OPTS="-Xms3G -Xmx6G -XX:MaxMetaspaceSize=2G" \ - mvn -pl obp-api -am clean package -DskipTests=true -Dmaven.test.skip=true && \ - java -jar obp-api/target/obp-api.jar -``` - -Binds to `hostname` / `dev.port` from your props file (defaults: `127.0.0.1:8080`). - ---- - -## Done Criteria - -| Milestone | Condition | Status | -|---|---|---| -| Version file done | All functional endpoints are `HttpRoutes[IO]`; the version's test suite is green. | ✅ all 12 | -| Lift bridge removed | All APIMethods files + auth stack + resource-docs done; `Http4sLiftWebBridge` deleted. | ✅ done | -| Lift Web removed | `lift-webkit` out of `pom.xml`; `Boot.scala` reduced to DB init + scheduler/shutdown. | ✅ done | -| `lift-mapper` removed | `net.liftweb:*` fully out of the dependency graph. | ⏳ separate long-term effort | diff --git a/LIFT_HTTP4S_MIGRATION_V6_AUDIT.md b/LIFT_HTTP4S_MIGRATION_V6_AUDIT.md deleted file mode 100644 index 610f6896f3..0000000000 --- a/LIFT_HTTP4S_MIGRATION_V6_AUDIT.md +++ /dev/null @@ -1,650 +0,0 @@ -# v6.0.0 Lift → http4s Migration: Override Audit & Batch Roadmap - -Companion to `LIFT_HTTP4S_MIGRATION.md`. Generated by static analysis of -`APIMethods*.scala` ResourceDoc declarations across every prior version. - -## Summary - -- **Total v6.0.0 endpoints**: 243 -- **Overrides** (same VERB + URL as an earlier version): **35** - - These MUST be migrated before `Http4s600` is wired into `Http4sApp.baseServices`. - - Reason: the bridge cascade would otherwise route v6 requests to v5/v4/etc handlers - silently. See CLAUDE.md → "Bridge-cascade hijack". -- **Originals** (new to v6, no earlier definition): **208** - - Safe to migrate in any order before OR after `Http4s600` is wired in. - -### Verb distribution - -| Verb | Overrides | Originals | Total | -|---|---|---|---| -| GET | 23 | 100 | 123 | -| POST | 8 | 51 | 59 | -| PUT | 4 | 30 | 34 | -| DELETE | 0 | 27 | 27 | -| **Total** | **35** | **208** | **243** | - ---- - -## The 35 override endpoints — must migrate before wire-in - -Sorted by verb then URL. - -| # | v6 endpoint | Verb | URL | Earlier versions defining same (verb, URL) | -|---|---|---|---|---| -| 1 | `getScannedApiVersions` | GET | `/api/versions` | v4_0_0 | -| 2 | `getBanks` | GET | `/banks` | v1_2_1, v3_0_0, v4_0_0 | -| 3 | `getBank` | GET | `/banks/BANK_ID` | v1_2_1, v3_0_0, v4_0_0, v5_0_0 | -| 4 | `getAccountsAtBank` | GET | `/banks/BANK_ID/accounts` | v1_2_1, v2_0_0, v4_0_0 | -| 5 | `getPrivateAccountByIdFull` | GET | `/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account` | v1_2_1, v2_0_0, v3_0_0, v3_1_0, v4_0_0 | -| 6 | `getTransactionsForBankAccount` | GET | `/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions` | v1_2_1, v3_0_0 | -| 7 | `getCustomersAtOneBank` | GET | `/banks/BANK_ID/customers` | v2_1_0, v4_0_0, v5_0_0 | -| 8 | `getCustomerByCustomerId` | GET | `/banks/BANK_ID/customers/CUSTOMER_ID` | v3_1_0 | -| 9 | `getProductsV600` | GET | `/banks/BANK_ID/products` | v1_4_0, v2_1_0, v3_1_0, v4_0_0 | -| 10 | `getCustomersAtAllBanks` | GET | `/customers` | v4_0_0 | -| 11 | `getAggregateMetrics` | GET | `/management/aggregate-metrics` | v3_0_0, v5_1_0 | -| 12 | `getBankLevelDynamicEntities` | GET | `/management/banks/BANK_ID/dynamic-entities` | v4_0_0 | -| 13 | `getConsumer` | GET | `/management/consumers/CONSUMER_ID` | v2_1_0, v3_1_0, v5_1_0 | -| 14 | `getMetrics` | GET | `/management/metrics` | v2_1_0, v5_1_0 | -| 15 | `getTopAPIs` | GET | `/management/metrics/top-apis` | v3_1_0 | -| 16 | `getSystemDynamicEntities` | GET | `/management/system-dynamic-entities` | v4_0_0 | -| 17 | `getCoreAccountByIdV600` | GET | `/my/banks/BANK_ID/accounts/ACCOUNT_ID/account` | v2_0_0, v3_0_0, v4_0_0 | -| 18 | `getMyDynamicEntities` | GET | `/my/dynamic-entities` | v4_0_0 | -| 19 | `root` | GET | `/root` | v1_2_1, v1_3_0, v1_4_0, v2_0_0, v2_1_0, v2_2_0, v3_0_0, v3_1_0, v4_0_0, v5_0_0, v5_1_0 | -| 20 | `getUsers` | GET | `/users` | v2_1_0, v3_0_0, v4_0_0 | -| 21 | `getUserAttributes` | GET | `/users/USER_ID/attributes` | v4_0_0 | -| 22 | `getCurrentUser` | GET | `/users/current` | v2_0_0, v3_0_0 | -| 23 | `getWebUiProps` | GET | `/webui-props` | v5_1_0 | -| 24 | `createBank` | POST | `/banks` | v2_2_0, v4_0_0, v5_0_0 | -| 25 | `createCustomer` | POST | `/banks/BANK_ID/customers` | v2_0_0, v2_1_0, v3_1_0, v4_0_0, v5_0_0 | -| 26 | `getCustomerByCustomerNumber` | POST | `/banks/BANK_ID/customers/customer-number` | v3_1_0 | -| 27 | `getCustomersByLegalName` | POST | `/banks/BANK_ID/customers/legal-name` | v5_1_0 | -| 28 | `createBankLevelDynamicEntity` | POST | `/management/banks/BANK_ID/dynamic-entities` | v4_0_0 | -| 29 | `createSystemDynamicEntity` | POST | `/management/system-dynamic-entities` | v4_0_0 | -| 30 | `resetPasswordUrl` | POST | `/management/user/reset-password-url` | v4_0_0 | -| 31 | `createUser` | POST | `/users` | v2_0_0 | -| 32 | `updateBankLevelDynamicEntity` | PUT | `/management/banks/BANK_ID/dynamic-entities/DYNAMIC_ENTITY_ID` | v4_0_0 | -| 33 | `updateSystemDynamicEntity` | PUT | `/management/system-dynamic-entities/DYNAMIC_ENTITY_ID` | v4_0_0 | -| 34 | `updateMyDynamicEntity` | PUT | `/my/dynamic-entities/DYNAMIC_ENTITY_ID` | v4_0_0 | -| 35 | `updateSystemView` | PUT | `/system-views/VIEW_ID` | v3_1_0, v5_0_0 | - ---- - -## The 208 original endpoints — grouped by domain - -Each domain is a natural batch boundary. Recommended PR size: 5–10 endpoints. -Buckets are sorted by size (largest domain first). - -### Bucket summary - -| Bucket | Count | Verbs | -|---|---|---| -| `chat-rooms` | 26 | DELETE:4 GET:9 POST:6 PUT:7 | -| `banks/.../chat-rooms` | 24 | DELETE:4 GET:8 POST:5 PUT:7 | -| `banks/.../accounts` | 22 | DELETE:2 GET:9 POST:9 PUT:2 | -| `users` | 16 | DELETE:2 GET:6 POST:6 PUT:2 | -| `banks/.../mandates` | 10 | DELETE:2 GET:4 POST:2 PUT:2 | -| `banks/.../api-products` | 9 | DELETE:2 GET:3 POST:2 PUT:2 | -| `system` | 8 | GET:8 | -| `management/abac-rules` | 8 | DELETE:1 GET:3 POST:3 PUT:1 | -| `management/consumers` | 6 | DELETE:1 GET:3 POST:1 PUT:1 | -| `management/groups` | 6 | DELETE:1 GET:3 POST:1 PUT:1 | -| `signal` | 6 | DELETE:1 GET:4 POST:1 | -| `my/personal-data-fields` | 5 | DELETE:1 GET:2 POST:1 PUT:1 | -| `banks/.../customer-links` | 5 | DELETE:1 GET:2 POST:1 PUT:1 | -| `banks/.../corporate-customers` | 4 | GET:3 POST:1 | -| `management/api-collections` | 4 | DELETE:1 GET:1 POST:1 PUT:1 | -| `banks/.../customers` | 3 | GET:3 | -| `banks/.../retail-customers` | 3 | GET:2 POST:1 | -| `management/banks` | 3 | GET:1 POST:2 | -| `management/diagnostics` | 2 | DELETE:1 GET:1 | -| `management/system-views` | 2 | GET:2 | -| `management/webui_props` | 2 | DELETE:1 PUT:1 | -| `management/system-dynamic-entities` | 2 | DELETE:1 POST:1 | -| `management/abac-policies` | 2 | GET:1 POST:1 | -| `oidc` | 2 | GET:1 POST:1 | -| `management/connector` | 2 | GET:2 | -| `banks/.../products` | 2 | GET:1 PUT:1 | -| `features` | 1 | GET:1 | -| `consumers` | 1 | GET:1 | -| `management/cache` | 1 | POST:1 | -| `management/dynamic-entities` | 1 | GET:1 | -| `providers` | 1 | GET:1 | -| `my/logins` | 1 | POST:1 | -| `entitlements` | 1 | DELETE:1 | -| `management/roles-with-entitlement-counts` | 1 | GET:1 | -| `management/view-permissions` | 1 | GET:1 | -| `management/custom-views` | 1 | GET:1 | -| `webui-props` | 1 | GET:1 | -| `management/abac-rules-schema` | 1 | GET:1 | -| `management/dynamic-resource-docs` | 1 | POST:1 | -| `message-docs` | 1 | GET:1 | -| `personal-dynamic-entities` | 1 | GET:1 | -| `api` | 1 | GET:1 | -| `api-products` | 1 | GET:1 | -| `products` | 1 | GET:1 | -| `management/config-props` | 1 | GET:1 | -| `app-directory` | 1 | GET:1 | -| `my/account-access-requests` | 1 | GET:1 | -| `banks/.../account-directory` | 1 | GET:1 | -| `banks/.../chat-room-participants` | 1 | POST:1 | -| `chat-room-participants` | 1 | POST:1 | - -### Full breakdown by bucket - -#### `chat-rooms` — 26 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteSystemChatRoom` | DELETE | `/chat-rooms/CHAT_ROOM_ID` | -| `deleteSystemChatMessage` | DELETE | `/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID` | -| `removeSystemReaction` | DELETE | `/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/reactions/EMOJI` | -| `removeSystemChatRoomParticipant` | DELETE | `/chat-rooms/CHAT_ROOM_ID/participants/USER_ID` | -| `getSystemChatRooms` | GET | `/chat-rooms` | -| `getSystemChatRoom` | GET | `/chat-rooms/CHAT_ROOM_ID` | -| `getSystemChatMessages` | GET | `/chat-rooms/CHAT_ROOM_ID/messages` | -| `getSystemChatMessage` | GET | `/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID` | -| `getSystemReactions` | GET | `/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/reactions` | -| `getSystemThreadReplies` | GET | `/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/thread` | -| `getBulkReactions` | GET | `/chat-rooms/CHAT_ROOM_ID/messages/reactions` | -| `getSystemChatRoomParticipants` | GET | `/chat-rooms/CHAT_ROOM_ID/participants` | -| `getSystemTypingUsers` | GET | `/chat-rooms/CHAT_ROOM_ID/typing-indicators` | -| `createSystemChatRoom` | POST | `/chat-rooms` | -| `sendSystemChatMessage` | POST | `/chat-rooms/CHAT_ROOM_ID/messages` | -| `addSystemReaction` | POST | `/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/reactions` | -| `replyInSystemThread` | POST | `/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/thread` | -| `addSystemChatRoomParticipant` | POST | `/chat-rooms/CHAT_ROOM_ID/participants` | -| `searchChatRooms` | POST | `/chat-rooms/search` | -| `updateSystemChatRoom` | PUT | `/chat-rooms/CHAT_ROOM_ID` | -| `archiveSystemChatRoom` | PUT | `/chat-rooms/CHAT_ROOM_ID/archive-status` | -| `refreshSystemJoiningKey` | PUT | `/chat-rooms/CHAT_ROOM_ID/joining-key` | -| `editSystemChatMessage` | PUT | `/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID` | -| `setSystemChatRoomOpenRoom` | PUT | `/chat-rooms/CHAT_ROOM_ID/open-room` | -| `updateSystemParticipantPermissions` | PUT | `/chat-rooms/CHAT_ROOM_ID/participants/USER_ID` | -| `signalSystemTyping` | PUT | `/chat-rooms/CHAT_ROOM_ID/typing-indicators` | - -#### `banks/.../chat-rooms` — 24 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteBankChatRoom` | DELETE | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID` | -| `deleteBankChatMessage` | DELETE | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID` | -| `removeBankReaction` | DELETE | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/reactions/EMOJI` | -| `removeBankChatRoomParticipant` | DELETE | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/participants/USER_ID` | -| `getBankChatRooms` | GET | `/banks/BANK_ID/chat-rooms` | -| `getBankChatRoom` | GET | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID` | -| `getBankChatMessages` | GET | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages` | -| `getBankChatMessage` | GET | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID` | -| `getBankReactions` | GET | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/reactions` | -| `getBankThreadReplies` | GET | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/thread` | -| `getBankChatRoomParticipants` | GET | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/participants` | -| `getBankTypingUsers` | GET | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/typing-indicators` | -| `createBankChatRoom` | POST | `/banks/BANK_ID/chat-rooms` | -| `sendBankChatMessage` | POST | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages` | -| `addBankReaction` | POST | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/reactions` | -| `replyInBankThread` | POST | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/thread` | -| `addBankChatRoomParticipant` | POST | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/participants` | -| `updateBankChatRoom` | PUT | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID` | -| `archiveBankChatRoom` | PUT | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/archive-status` | -| `refreshBankJoiningKey` | PUT | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/joining-key` | -| `editBankChatMessage` | PUT | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID` | -| `setBankChatRoomOpenRoom` | PUT | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/open-room` | -| `updateBankParticipantPermissions` | PUT | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/participants/USER_ID` | -| `signalBankTyping` | PUT | `/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/typing-indicators` | - -#### `banks/.../accounts` — 22 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteCounterpartyAttribute` | DELETE | `/banks/BANK_ID/accounts/ACCOUNT_ID/counterparties/COUNTERPARTY_ID/attributes/COUNTERPARTY_ATTRIBUTE_ID` | -| `deleteMandate` | DELETE | `/banks/BANK_ID/accounts/ACCOUNT_ID/mandates/MANDATE_ID` | -| `getHoldingAccountByReleaser` | GET | `/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/holding-accounts` | -| `getAccountAccessRequestsForAccount` | GET | `/banks/BANK_ID/accounts/ACCOUNT_ID/account-access-requests` | -| `getAccountAccessRequestById` | GET | `/banks/BANK_ID/accounts/ACCOUNT_ID/account-access-requests/ACCOUNT_ACCESS_REQUEST_ID` | -| `getAllCounterpartyAttributes` | GET | `/banks/BANK_ID/accounts/ACCOUNT_ID/counterparties/COUNTERPARTY_ID/attributes` | -| `getCounterpartyAttributeById` | GET | `/banks/BANK_ID/accounts/ACCOUNT_ID/counterparties/COUNTERPARTY_ID/attributes/COUNTERPARTY_ATTRIBUTE_ID` | -| `getMandates` | GET | `/banks/BANK_ID/accounts/ACCOUNT_ID/mandates` | -| `getMandate` | GET | `/banks/BANK_ID/accounts/ACCOUNT_ID/mandates/MANDATE_ID` | -| `hasAccountAccess` | GET | `/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/has-account-access` | -| `getUsersWithAccountAccess` | GET | `/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID/users-with-access` | -| `createAccountAccessRequest` | POST | `/banks/BANK_ID/accounts/ACCOUNT_ID/account-access-requests` | -| `approveAccountAccessRequest` | POST | `/banks/BANK_ID/accounts/ACCOUNT_ID/account-access-requests/ACCOUNT_ACCESS_REQUEST_ID/approval` | -| `rejectAccountAccessRequest` | POST | `/banks/BANK_ID/accounts/ACCOUNT_ID/account-access-requests/ACCOUNT_ACCESS_REQUEST_ID/rejection` | -| `createCounterpartyAttribute` | POST | `/banks/BANK_ID/accounts/ACCOUNT_ID/counterparties/COUNTERPARTY_ID/attributes` | -| `createMandate` | POST | `/banks/BANK_ID/accounts/ACCOUNT_ID/mandates` | -| `createTransactionRequestCardano` | POST | `/banks/BANK_ID/accounts/ACCOUNT_ID/owner/transaction-request-types/CARDANO/transaction-requests` | -| `createTransactionRequestEthSendRawTransaction` | POST | `/banks/BANK_ID/accounts/ACCOUNT_ID/owner/transaction-request-types/ETH_SEND_RAW_TRANSACTION/transaction-requests` | -| `createTransactionRequestEthereumeSendTransaction` | POST | `/banks/BANK_ID/accounts/ACCOUNT_ID/owner/transaction-request-types/ETH_SEND_TRANSACTION/transaction-requests` | -| `createTransactionRequestHold` | POST | `/banks/BANK_ID/accounts/ACCOUNT_ID/owner/transaction-request-types/HOLD/transaction-requests` | -| `updateCounterpartyAttribute` | PUT | `/banks/BANK_ID/accounts/ACCOUNT_ID/counterparties/COUNTERPARTY_ID/attributes/COUNTERPARTY_ATTRIBUTE_ID` | -| `updateMandate` | PUT | `/banks/BANK_ID/accounts/ACCOUNT_ID/mandates/MANDATE_ID` | - -#### `users` — 16 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteUserAttribute` | DELETE | `/users/USER_ID/attributes/USER_ATTRIBUTE_ID` | -| `removeUserFromGroup` | DELETE | `/users/USER_ID/group-entitlements/GROUP_ID` | -| `getUserAttributeById` | GET | `/users/USER_ID/attributes/USER_ATTRIBUTE_ID` | -| `getUserGroupMemberships` | GET | `/users/USER_ID/group-entitlements` | -| `getMyChatRooms` | GET | `/users/current/chat-rooms` | -| `getMyUnreadCounts` | GET | `/users/current/chat-rooms/unread` | -| `getMyMentions` | GET | `/users/current/mentions` | -| `getUserByUserId` | GET | `/users/user-id/USER_ID` | -| `createUserAttribute` | POST | `/users/USER_ID/attributes` | -| `addUserToGroup` | POST | `/users/USER_ID/group-entitlements` | -| `validateUserEmail` | POST | `/users/email-validation` | -| `resetPasswordComplete` | POST | `/users/password` | -| `resetPasswordUrlAnonymous` | POST | `/users/password-reset-url` | -| `verifyUserCredentials` | POST | `/users/verify-credentials` | -| `updateUserAttribute` | PUT | `/users/USER_ID/attributes/USER_ATTRIBUTE_ID` | -| `markChatRoomRead` | PUT | `/users/current/chat-rooms/CHAT_ROOM_ID/read-marker` | - -#### `banks/.../mandates` — 10 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteMandateProvision` | DELETE | `/banks/BANK_ID/mandates/MANDATE_ID/provisions/PROVISION_ID` | -| `deleteSignatoryPanel` | DELETE | `/banks/BANK_ID/mandates/MANDATE_ID/signatory-panels/PANEL_ID` | -| `getMandateProvisions` | GET | `/banks/BANK_ID/mandates/MANDATE_ID/provisions` | -| `getMandateProvision` | GET | `/banks/BANK_ID/mandates/MANDATE_ID/provisions/PROVISION_ID` | -| `getSignatoryPanels` | GET | `/banks/BANK_ID/mandates/MANDATE_ID/signatory-panels` | -| `getSignatoryPanel` | GET | `/banks/BANK_ID/mandates/MANDATE_ID/signatory-panels/PANEL_ID` | -| `createMandateProvision` | POST | `/banks/BANK_ID/mandates/MANDATE_ID/provisions` | -| `createSignatoryPanel` | POST | `/banks/BANK_ID/mandates/MANDATE_ID/signatory-panels` | -| `updateMandateProvision` | PUT | `/banks/BANK_ID/mandates/MANDATE_ID/provisions/PROVISION_ID` | -| `updateSignatoryPanel` | PUT | `/banks/BANK_ID/mandates/MANDATE_ID/signatory-panels/PANEL_ID` | - -#### `banks/.../api-products` — 9 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteApiProduct` | DELETE | `/banks/BANK_ID/api-products/API_PRODUCT_CODE` | -| `deleteApiProductAttribute` | DELETE | `/banks/BANK_ID/api-products/API_PRODUCT_CODE/attributes/API_PRODUCT_ATTRIBUTE_ID` | -| `getApiProducts` | GET | `/banks/BANK_ID/api-products` | -| `getApiProduct` | GET | `/banks/BANK_ID/api-products/API_PRODUCT_CODE` | -| `getApiProductAttribute` | GET | `/banks/BANK_ID/api-products/API_PRODUCT_CODE/attributes/API_PRODUCT_ATTRIBUTE_ID` | -| `createApiProduct` | POST | `/banks/BANK_ID/api-products/API_PRODUCT_CODE` | -| `createApiProductAttribute` | POST | `/banks/BANK_ID/api-products/API_PRODUCT_CODE/attribute` | -| `createOrUpdateApiProduct` | PUT | `/banks/BANK_ID/api-products/API_PRODUCT_CODE` | -| `updateApiProductAttribute` | PUT | `/banks/BANK_ID/api-products/API_PRODUCT_CODE/attributes/API_PRODUCT_ATTRIBUTE_ID` | - -#### `system` — 8 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getCacheConfig` | GET | `/system/cache/config` | -| `getCacheInfo` | GET | `/system/cache/info` | -| `getCacheNamespaces` | GET | `/system/cache/namespaces` | -| `getConnectorMethodNames` | GET | `/system/connector-method-names` | -| `getConnectors` | GET | `/system/connectors` | -| `getStoredProcedureConnectorHealth` | GET | `/system/connectors/stored_procedure_vDec2019/health` | -| `getDatabasePoolInfo` | GET | `/system/database/pool` | -| `getMigrations` | GET | `/system/migrations` | - -#### `management/abac-rules` — 8 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteAbacRule` | DELETE | `/management/abac-rules/ABAC_RULE_ID` | -| `getAbacRules` | GET | `/management/abac-rules` | -| `getAbacRule` | GET | `/management/abac-rules/ABAC_RULE_ID` | -| `getAbacRulesByPolicy` | GET | `/management/abac-rules/policy/POLICY` | -| `createAbacRule` | POST | `/management/abac-rules` | -| `executeAbacRule` | POST | `/management/abac-rules/ABAC_RULE_ID/execute` | -| `validateAbacRule` | POST | `/management/abac-rules/validate` | -| `updateAbacRule` | PUT | `/management/abac-rules/ABAC_RULE_ID` | - -#### `management/consumers` — 6 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteCallLimits` | DELETE | `/management/consumers/CONSUMER_ID/consumer/rate-limits/RATE_LIMITING_ID` | -| `getActiveRateLimitsNow` | GET | `/management/consumers/CONSUMER_ID/active-rate-limits` | -| `getActiveRateLimitsAtDate` | GET | `/management/consumers/CONSUMER_ID/active-rate-limits/DATE_WITH_HOUR` | -| `getConsumerCallCounters` | GET | `/management/consumers/CONSUMER_ID/call-counters` | -| `createCallLimits` | POST | `/management/consumers/CONSUMER_ID/consumer/rate-limits` | -| `updateRateLimits` | PUT | `/management/consumers/CONSUMER_ID/consumer/rate-limits/RATE_LIMITING_ID` | - -#### `management/groups` — 6 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteGroup` | DELETE | `/management/groups/GROUP_ID` | -| `getGroups` | GET | `/management/groups` | -| `getGroup` | GET | `/management/groups/GROUP_ID` | -| `getGroupEntitlements` | GET | `/management/groups/GROUP_ID/entitlements` | -| `createGroup` | POST | `/management/groups` | -| `updateGroup` | PUT | `/management/groups/GROUP_ID` | - -#### `signal` — 6 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteSignalChannel` | DELETE | `/signal/channels/CHANNEL_NAME` | -| `getSignalChannels` | GET | `/signal/channels` | -| `getSignalChannelInfo` | GET | `/signal/channels/CHANNEL_NAME/info` | -| `getSignalMessages` | GET | `/signal/channels/CHANNEL_NAME/messages` | -| `getSignalStats` | GET | `/signal/channels/stats` | -| `publishSignalMessage` | POST | `/signal/channels/CHANNEL_NAME/messages` | - -#### `my/personal-data-fields` — 5 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deletePersonalDataField` | DELETE | `/my/personal-data-fields/USER_ATTRIBUTE_ID` | -| `getPersonalDataFields` | GET | `/my/personal-data-fields` | -| `getPersonalDataFieldById` | GET | `/my/personal-data-fields/USER_ATTRIBUTE_ID` | -| `createPersonalDataField` | POST | `/my/personal-data-fields` | -| `updatePersonalDataField` | PUT | `/my/personal-data-fields/USER_ATTRIBUTE_ID` | - -#### `banks/.../customer-links` — 5 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteCustomerLink` | DELETE | `/banks/BANK_ID/customer-links/CUSTOMER_LINK_ID` | -| `getCustomerLinksByBankId` | GET | `/banks/BANK_ID/customer-links` | -| `getCustomerLinkById` | GET | `/banks/BANK_ID/customer-links/CUSTOMER_LINK_ID` | -| `createCustomerLink` | POST | `/banks/BANK_ID/customer-links` | -| `updateCustomerLink` | PUT | `/banks/BANK_ID/customer-links/CUSTOMER_LINK_ID` | - -#### `banks/.../corporate-customers` — 4 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getCorporateCustomersAtOneBank` | GET | `/banks/BANK_ID/corporate-customers` | -| `getCorporateCustomerByCustomerId` | GET | `/banks/BANK_ID/corporate-customers/CUSTOMER_ID` | -| `getCorporateCustomerSubsidiaries` | GET | `/banks/BANK_ID/corporate-customers/CUSTOMER_ID/subsidiaries` | -| `createCorporateCustomer` | POST | `/banks/BANK_ID/corporate-customers` | - -#### `management/api-collections` — 4 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteFeaturedApiCollection` | DELETE | `/management/api-collections/featured/API_COLLECTION_ID` | -| `getFeaturedApiCollectionsAdmin` | GET | `/management/api-collections/featured` | -| `createFeaturedApiCollection` | POST | `/management/api-collections/featured` | -| `updateFeaturedApiCollection` | PUT | `/management/api-collections/featured/API_COLLECTION_ID` | - -#### `banks/.../customers` — 3 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getCustomerChildren` | GET | `/banks/BANK_ID/customers/CUSTOMER_ID/children` | -| `getCustomerLinksByCustomerId` | GET | `/banks/BANK_ID/customers/CUSTOMER_ID/customer-links` | -| `getCustomerInvestigationReport` | GET | `/banks/BANK_ID/customers/CUSTOMER_ID/investigation-report` | - -#### `banks/.../retail-customers` — 3 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getRetailCustomersAtOneBank` | GET | `/banks/BANK_ID/retail-customers` | -| `getRetailCustomerByCustomerId` | GET | `/banks/BANK_ID/retail-customers/CUSTOMER_ID` | -| `createRetailCustomer` | POST | `/banks/BANK_ID/retail-customers` | - -#### `management/banks` — 3 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getCustomViewById` | GET | `/management/banks/BANK_ID/accounts/ACCOUNT_ID/views/VIEW_ID` | -| `createCustomViewManagement` | POST | `/management/banks/BANK_ID/accounts/ACCOUNT_ID/views` | -| `backupBankLevelDynamicEntity` | POST | `/management/banks/BANK_ID/dynamic-entities/DYNAMIC_ENTITY_ID/backup` | - -#### `management/diagnostics` — 2 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `cleanupOrphanedDynamicEntityRecords` | DELETE | `/management/diagnostics/dynamic-entities/orphaned-records` | -| `getDynamicEntityDiagnostics` | GET | `/management/diagnostics/dynamic-entities` | - -#### `management/system-views` — 2 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getSystemViews` | GET | `/management/system-views` | -| `getSystemViewById` | GET | `/management/system-views/VIEW_ID` | - -#### `management/webui_props` — 2 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteWebUiProps` | DELETE | `/management/webui_props/WEBUI_PROP_NAME` | -| `createOrUpdateWebUiProps` | PUT | `/management/webui_props/WEBUI_PROP_NAME` | - -#### `management/system-dynamic-entities` — 2 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteSystemDynamicEntityCascade` | DELETE | `/management/system-dynamic-entities/cascade/DYNAMIC_ENTITY_ID` | -| `backupSystemDynamicEntity` | POST | `/management/system-dynamic-entities/DYNAMIC_ENTITY_ID/backup` | - -#### `management/abac-policies` — 2 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getAbacPolicies` | GET | `/management/abac-policies` | -| `executeAbacPolicy` | POST | `/management/abac-policies/POLICY/execute` | - -#### `oidc` — 2 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getOidcClient` | GET | `/oidc/clients/CLIENT_ID` | -| `verifyOidcClient` | POST | `/oidc/clients/verify` | - -#### `management/connector` — 2 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getConnectorCallCounts` | GET | `/management/connector/metrics/counts` | -| `getConnectorTraces` | GET | `/management/connector/traces` | - -#### `banks/.../products` — 2 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getProductTagsV600` | GET | `/banks/BANK_ID/products/PRODUCT_CODE/tags` | -| `updateProductTagsV600` | PUT | `/banks/BANK_ID/products/PRODUCT_CODE/tags` | - -#### `features` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getFeatures` | GET | `/features` | - -#### `consumers` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getCurrentConsumer` | GET | `/consumers/current` | - -#### `management/cache` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `invalidateCacheNamespace` | POST | `/management/cache/namespaces/invalidate` | - -#### `management/dynamic-entities` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getReferenceTypes` | GET | `/management/dynamic-entities/reference-types` | - -#### `providers` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getProviders` | GET | `/providers` | - -#### `my/logins` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `directLoginEndpoint` | POST | `/my/logins/direct` | - -#### `entitlements` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `deleteEntitlement` | DELETE | `/entitlements/ENTITLEMENT_ID` | - -#### `management/roles-with-entitlement-counts` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getRolesWithEntitlementCountsAtAllBanks` | GET | `/management/roles-with-entitlement-counts` | - -#### `management/view-permissions` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getViewPermissions` | GET | `/management/view-permissions` | - -#### `management/custom-views` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getCustomViews` | GET | `/management/custom-views` | - -#### `webui-props` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getWebUiProp` | GET | `/webui-props/WEBUI_PROP_NAME` | - -#### `management/abac-rules-schema` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getAbacRuleSchema` | GET | `/management/abac-rules-schema` | - -#### `management/dynamic-resource-docs` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `validateDynamicResourceDoc` | POST | `/management/dynamic-resource-docs/validate` | - -#### `message-docs` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getMessageDocsJsonSchema` | GET | `/message-docs/CONNECTOR/json-schema` | - -#### `personal-dynamic-entities` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getAvailablePersonalDynamicEntities` | GET | `/personal-dynamic-entities/available` | - -#### `api` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getPopularApis` | GET | `/api/popular-endpoints` | - -#### `api-products` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getAllApiProductsV600` | GET | `/api-products` | - -#### `products` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getAllProductsV600` | GET | `/products` | - -#### `management/config-props` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getConfigProps` | GET | `/management/config-props` | - -#### `app-directory` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getAppDirectory` | GET | `/app-directory` | - -#### `my/account-access-requests` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getMyAccountAccessRequests` | GET | `/my/account-access-requests` | - -#### `banks/.../account-directory` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `getAccountDirectory` | GET | `/banks/BANK_ID/account-directory` | - -#### `banks/.../chat-room-participants` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `joinBankChatRoom` | POST | `/banks/BANK_ID/chat-room-participants` | - -#### `chat-room-participants` — 1 endpoints - -| Endpoint | Verb | URL | -|---|---|---| -| `joinSystemChatRoom` | POST | `/chat-room-participants` | - ---- - -## Recommended migration order - -**Phase 0 — Foundation (1 PR, ~1 day)** - -1. Create `obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala` skeleton: - `prefixPath = Root / "obp" / "v6.0.0"`, empty `allRoutes`, `v600ToV510Bridge` - (path-rewrite to v5.1.0, then through the existing cascade). -2. Do NOT add to `Http4sApp.baseServices` yet — it remains inert. -3. Add `Http4s600.scala` registration to `OBPAPI6_0_0.allResourceDocs` chain - for resource-docs aggregation parity, but keep `resourceDocs` empty. - -**Phase 1 — Override batch (3–5 PRs, ~2–3 weeks)** - -Migrate the 35 overrides in 4 PRs by verb cluster, in this order: - -- PR 1: All 23 GET overrides (mechanical, lowest risk) -- PR 2: 4 PUT overrides -- PR 3: 8 POST overrides -- PR 4: **Wire `Http4s600` into `Http4sApp.baseServices`** + full regression run - -After PR 4, the chain is `… → Http4s600 → v600ToV510Bridge → Http4s510 → …` -and overrides are protected. - -**Phase 2 — Originals by domain (~20 PRs, ~3–5 weeks)** - -Migrate originals one bucket per PR (or split large buckets): - -- PR 5–6: `chat-rooms` (26) + `banks/.../chat-rooms` (24) — biggest domain (50 endpoints) -- PR 7: `banks/.../accounts` (22) -- PR 8: `users` (16) -- PR 9: `banks/.../mandates` (10) -- PR 10: `banks/.../api-products` (9) + `management/abac-rules` (8) -- PR 11: `system` (8) — note these are 8 GETs on `/system-*` paths -- PR 12–13: remaining management/* buckets -- PR 14+: smaller buckets in batches of 5–10 endpoints - -**Phase 3 — Cleanup** - -- Audit `disableAutoValidateRoles()` calls in v6 Lift sources for any inline-role-check - patterns (CLAUDE.md "Bypass roles vs required roles"). -- Verify `excludeEndpoints` list in `OBPAPI6_0_0` matches the migrated set. -- Remove unused Lift implementations as they become dead. - ---- - -## Estimated effort - -Using the CLAUDE.md velocity figures (6–8 endpoints/day mutations, faster for GETs): - -| Phase | Endpoints | PRs | Estimated days | -|---|---|---|---| -| 0 — Foundation | 0 | 1 | ~1 | -| 1 — Override batch | 35 | 3–5 | ~7–10 | -| 2 — Originals | 208 | ~20 | ~30–40 | -| **Total** | **243** | **~25** | **~38–51** | - -Roughly **8–10 weeks** of focused work. diff --git a/README.md b/README.md index 9f06c7d7b3..4c27f48cef 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,11 @@ sdk env install #### Manually -- OracleJDK: 1.8, 13 -- OpenJdk: 11 +The project targets **JDK 25 (LTS)** with Scala 2.12.21. Any OpenJDK 25 distribution works, +for example: -OpenJDK 11 is available for download here: [https://jdk.java.net/archive/](https://jdk.java.net/archive/). +- Eclipse Temurin 25: [https://adoptium.net/](https://adoptium.net/) +- Azul Zulu 25: [https://www.azul.com/downloads/](https://www.azul.com/downloads/) The project uses Maven 3 as its build tool. @@ -73,6 +74,12 @@ java -jar obp-api/target/obp-api.jar The http4s server binds to `hostname` / `dev.port` as configured in your props file (defaults are `127.0.0.1` and `8080`). +No `--add-opens` flags are needed on the command line: the executable jar's manifest carries +an `Add-Opens` attribute (JEP 261) with all modules the runtime needs (CGLib proxy generation, +Kryo serialization, Pekko remoting, Scala runtime reflection). Only when launching via a +custom classpath (`java -cp ... bootstrap.http4s.Http4sServer`) do the flags need to be passed +explicitly, since the manifest is only honored by `java -jar`. + ### ZED IDE Setup For ZED IDE users, we provide a complete development environment with Scala language server support: diff --git a/build.sbt b/build.sbt deleted file mode 100644 index add70f9eae..0000000000 --- a/build.sbt +++ /dev/null @@ -1,224 +0,0 @@ -ThisBuild / version := "1.10.1" -ThisBuild / scalaVersion := "2.12.20" -ThisBuild / organization := "com.tesobe" - -// Java version compatibility -ThisBuild / javacOptions ++= Seq("-source", "11", "-target", "11") -ThisBuild / scalacOptions ++= Seq( - "-unchecked", - "-explaintypes", - "-target:jvm-1.8", - "-Yrangepos" -) - -// Enable SemanticDB for Metals -ThisBuild / semanticdbEnabled := true -ThisBuild / semanticdbVersion := "4.13.9" - -// Fix dependency conflicts -ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always -ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-java8-compat" % VersionScheme.Always - -lazy val liftVersion = "3.5.0" -lazy val akkaVersion = "2.5.32" -lazy val jettyVersion = "9.4.50.v20221201" -lazy val avroVersion = "1.8.2" -lazy val pekkoVersion = "1.4.0" -lazy val pekkoHttpVersion = "1.3.0" -lazy val http4sVersion = "0.23.30" -lazy val catsEffectVersion = "3.5.7" -lazy val ip4sVersion = "3.7.0" -lazy val jakartaMailVersion = "2.0.1" - -lazy val commonSettings = Seq( - resolvers ++= Seq( - "Sonatype OSS Releases" at "https://oss.sonatype.org/content/repositories/releases", - "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots", - "Artima Maven Repository" at "https://repo.artima.com/releases", - "OpenBankProject M2 Repository" at "https://raw.githubusercontent.com/OpenBankProject/OBP-M2-REPO/master", - "jitpack.io" at "https://jitpack.io" - ) -) - -lazy val obpCommons = (project in file("obp-commons")) - .settings( - commonSettings, - name := "obp-commons", - libraryDependencies ++= Seq( - "net.liftweb" %% "lift-common" % liftVersion, - "net.liftweb" %% "lift-util" % liftVersion, - "net.liftweb" %% "lift-mapper" % liftVersion, - "org.scala-lang" % "scala-reflect" % "2.12.20", - "org.scalatest" %% "scalatest" % "3.0.9" % Test, - "org.scalactic" %% "scalactic" % "3.0.9", - "net.liftweb" %% "lift-json" % liftVersion, - "com.alibaba" % "transmittable-thread-local" % "2.11.5", - "org.apache.commons" % "commons-lang3" % "3.12.0", - "org.apache.commons" % "commons-text" % "1.10.0", - "com.google.guava" % "guava" % "32.0.0-jre" - ) - ) - -lazy val obpApi = (project in file("obp-api")) - .dependsOn(obpCommons) - .settings( - commonSettings, - name := "obp-api", - libraryDependencies ++= Seq( - // Core dependencies - "net.liftweb" %% "lift-mapper" % liftVersion, - "net.databinder.dispatch" %% "dispatch-lift-json" % "0.13.1", - "ch.qos.logback" % "logback-classic" % "1.2.13", - "org.slf4j" % "log4j-over-slf4j" % "1.7.26", - "org.slf4j" % "slf4j-ext" % "1.7.26", - - // Security - "org.bouncycastle" % "bcpg-jdk15on" % "1.70", - "org.bouncycastle" % "bcpkix-jdk15on" % "1.70", - "com.nimbusds" % "nimbus-jose-jwt" % "9.37.2", - "com.nimbusds" % "oauth2-oidc-sdk" % "9.27", - - // Commons - "org.apache.commons" % "commons-lang3" % "3.12.0", - "org.apache.commons" % "commons-text" % "1.10.0", - "org.apache.commons" % "commons-email" % "1.5", - "org.apache.commons" % "commons-compress" % "1.26.0", - "org.apache.commons" % "commons-pool2" % "2.11.1", - - // Database - "org.postgresql" % "postgresql" % "42.4.4", - "com.h2database" % "h2" % "2.2.220" % Runtime, - "mysql" % "mysql-connector-java" % "8.0.30", - "com.microsoft.sqlserver" % "mssql-jdbc" % "11.2.0.jre11", - - // Web - "javax.servlet" % "javax.servlet-api" % "3.1.0" % Provided, - "org.eclipse.jetty" % "jetty-server" % jettyVersion % Test, - "org.eclipse.jetty" % "jetty-webapp" % jettyVersion % Test, - "org.eclipse.jetty" % "jetty-util" % jettyVersion, - - // Akka - "com.typesafe.akka" %% "akka-actor" % akkaVersion, - "com.typesafe.akka" %% "akka-remote" % akkaVersion, - "com.typesafe.akka" %% "akka-slf4j" % akkaVersion, - "com.typesafe.akka" %% "akka-http-core" % "10.1.6", - - // Pekko (ActorSystem + Pekko HTTP used by OBP runtime components) - "org.apache.pekko" %% "pekko-actor" % pekkoVersion, - "org.apache.pekko" %% "pekko-remote" % pekkoVersion, - "org.apache.pekko" %% "pekko-slf4j" % pekkoVersion, - "org.apache.pekko" %% "pekko-stream" % pekkoVersion, - "org.apache.pekko" %% "pekko-http" % pekkoHttpVersion, - - // http4s (v7.0.0 experimental stack) - "org.typelevel" %% "cats-effect" % catsEffectVersion, - "com.comcast" %% "ip4s-core" % ip4sVersion, - "org.http4s" %% "http4s-core" % http4sVersion, - "org.http4s" %% "http4s-dsl" % http4sVersion, - "org.http4s" %% "http4s-ember-server" % http4sVersion, - - // Avro - "com.sksamuel.avro4s" %% "avro4s-core" % avroVersion, - - // Twitter - "com.twitter" %% "chill-akka" % "0.9.1", - "com.twitter" %% "chill-bijection" % "0.9.1", - - // Cache - "com.github.cb372" %% "scalacache-redis" % "0.9.3", - "com.github.cb372" %% "scalacache-guava" % "0.9.3", - - // Utilities - "com.github.dwickern" %% "scala-nameof" % "1.0.3", - "org.javassist" % "javassist" % "3.25.0-GA", - "com.alibaba" % "transmittable-thread-local" % "2.14.2", - "org.clapper" %% "classutil" % "1.4.0", - "com.github.grumlimited" % "geocalc" % "0.5.7", - "com.github.OpenBankProject" % "scala-macros" % "v1.0.0-alpha.3", - "org.scalameta" %% "scalameta" % "3.7.4", - - // Akka Adapter - exclude transitive dependency on obp-commons to use local module - "com.github.OpenBankProject.OBP-Adapter-Akka-SpringBoot" % "adapter-akka-commons" % "v1.1.0" exclude("com.github.OpenBankProject.OBP-API", "obp-commons"), - - // JSON Schema - "com.github.everit-org.json-schema" % "org.everit.json.schema" % "1.6.1", - "com.networknt" % "json-schema-validator" % "1.0.87", - - // Swagger - "io.swagger.parser.v3" % "swagger-parser" % "2.0.13", - - // Text processing - "org.atteo" % "evo-inflector" % "1.2.2", - - // Payment - "com.stripe" % "stripe-java" % "12.1.0", - "com.twilio.sdk" % "twilio" % "9.2.0", - - // gRPC - "com.thesamet.scalapb" %% "scalapb-runtime-grpc" % "0.8.4", - "io.grpc" % "grpc-all" % "1.48.1", - "io.netty" % "netty-tcnative-boringssl-static" % "2.0.27.Final", - "org.asynchttpclient" % "async-http-client" % "2.10.4", - - // Database utilities - "org.scalikejdbc" %% "scalikejdbc" % "3.4.0", - - // XML - "org.scala-lang.modules" %% "scala-xml" % "1.2.0", - - // IBAN - "org.iban4j" % "iban4j" % "3.2.7-RELEASE", - - // JavaScript - "org.graalvm.js" % "js" % "22.0.0.2", - "org.graalvm.js" % "js-scriptengine" % "22.0.0.2", - "ch.obermuhlner" % "java-scriptengine" % "2.0.0", - - // Hydra - "sh.ory.hydra" % "hydra-client" % "1.7.0", - - // HTTP - "com.squareup.okhttp3" % "okhttp" % "4.12.0", - "com.squareup.okhttp3" % "logging-interceptor" % "4.12.0", - "org.apache.httpcomponents" % "httpclient" % "4.5.13", - - // RabbitMQ - "com.rabbitmq" % "amqp-client" % "5.22.0", - "net.liftmodules" %% "amqp_3.1" % "1.5.0", - - // Blockchain (Ethereum raw transaction decoding) - "org.web3j" % "core" % "4.14.0", - - // Elasticsearch - "org.elasticsearch" % "elasticsearch" % "8.14.0", - "com.sksamuel.elastic4s" %% "elastic4s-client-esjava" % "8.5.2", - - // OAuth - "oauth.signpost" % "signpost-commonshttp4" % "1.2.1.2", - - // Utilities - "cglib" % "cglib" % "3.3.0", - "com.sun.activation" % "jakarta.activation" % "1.2.2", - "com.sun.mail" % "jakarta.mail" % jakartaMailVersion, - "com.nulab-inc" % "zxcvbn" % "1.9.0", - - // Testing - temporarily disabled due to version incompatibility - // "org.scalatest" %% "scalatest" % "2.2.6" % Test, - - // Jackson - "com.fasterxml.jackson.core" % "jackson-databind" % "2.12.7.1", - - // Flexmark (markdown processing) - "com.vladsch.flexmark" % "flexmark-profile-pegdown" % "0.40.8", - "com.vladsch.flexmark" % "flexmark-util-options" % "0.64.0", - - // Connection pool - "com.zaxxer" % "HikariCP" % "4.0.3", - - // Test dependencies - "junit" % "junit" % "4.13.2" % Test, - "org.scalatest" %% "scalatest" % "3.0.9" % Test, - "org.seleniumhq.selenium" % "htmlunit-driver" % "2.36.0" % Test, - "org.testcontainers" % "rabbitmq" % "1.20.3" % Test - ) - ) diff --git a/development/docker/Dockerfile b/development/docker/Dockerfile index d4b110e8ba..65af3bfb2b 100644 --- a/development/docker/Dockerfile +++ b/development/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM maven:3-eclipse-temurin-11 as maven +FROM maven:3-eclipse-temurin-25 as maven # Build the source using maven, source is copied from the 'repo' build. COPY . /usr/src/OBP-API RUN cp /usr/src/OBP-API/obp-api/pom.xml /tmp/pom.xml # For Packaging a local repository within the image @@ -8,6 +8,6 @@ RUN cp obp-api/src/main/resources/props/sample.props.template obp-api/src/main/r RUN --mount=type=cache,target=$HOME/.m2 MAVEN_OPTS="-Xmx3G -Xss2m" mvn install -pl .,obp-commons RUN --mount=type=cache,target=$HOME/.m2 MAVEN_OPTS="-Xmx3G -Xss2m" mvn install -DskipTests -pl obp-api -FROM eclipse-temurin:11-jre-alpine +FROM eclipse-temurin:25-jre-alpine COPY --from=maven /usr/src/OBP-API/obp-api/target/obp-api.jar /app/obp-api.jar ENTRYPOINT ["java", "-jar", "/app/obp-api.jar"] \ No newline at end of file diff --git a/development/docker/Dockerfile.dev b/development/docker/Dockerfile.dev index d24ca0f644..cf5e24a76b 100644 --- a/development/docker/Dockerfile.dev +++ b/development/docker/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM maven:3.9.6-eclipse-temurin-17 +FROM maven:3.9.16-eclipse-temurin-25 WORKDIR /app diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 66bf746e62..1ba74f66f1 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -32,12 +32,6 @@ com.github.OpenBankProject.lift-persistence lift-persistence_${scala.version} - - net.databinder.dispatch - dispatch-core_${scala.version} - 0.13.1 - test - org.json4s json4s-native_${scala.version} @@ -369,7 +363,11 @@ org.javassist javassist - 3.25.0-GA + + 3.32.0-GA @@ -403,18 +401,6 @@ grpc-services 1.75.0 - - org.asynchttpclient - async-http-client - 2.15.0 - test - - - javax.activation - com.sun.activation - - - @@ -432,7 +418,7 @@ com.microsoft.sqlserver mssql-jdbc - 13.4.0.jre${java.version} + 13.4.0.jre${mssql.jre.version} - org.graalvm.js - js - 22.3.3 + org.graalvm.polyglot + polyglot + 24.1.2 org.graalvm.js - js-scriptengine - 22.3.3 + js-language + 24.1.2 + + + org.graalvm.truffle + truffle-api + 24.1.2 + + + org.graalvm.truffle + truffle-runtime + 24.1.2 + + + org.graalvm.regex + regex + 24.1.2 + + + org.graalvm.shadowed + icu4j + 24.1.2 @@ -738,6 +744,14 @@ bootstrap.http4s.Http4sServer + + + java.base/java.lang java.base/java.lang.reflect java.base/java.util java.base/java.lang.invoke java.base/java.util.jar java.base/sun.reflect.generics.reflectiveObjects java.base/java.io java.base/java.util.concurrent + reference.conf diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 36eae74d29..a51be6b96b 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -163,6 +163,7 @@ import java.io.{File, FileInputStream} import java.util.stream.Collectors import java.util.{Locale, TimeZone} import scala.concurrent.ExecutionContext +import scala.util.control.NonFatal @@ -551,7 +552,11 @@ class Boot extends MdcLoggable { if(useMessageQueue) BankAccountCreationListener.startListen } catch { + // ExceptionInInitializerError is a LinkageError, so NonFatal does not cover it. + // NonFatal covers e.g. java.net.ConnectException when the broker is unreachable; + // without it the exception escapes boot, the main thread dies and the server never binds. case e: ExceptionInInitializerError => logger.warn(s"BankAccountCreationListener Exception: $e") + case NonFatal(e) => logger.warn(s"BankAccountCreationListener Exception: $e") } if ( !APIUtil.getPropsAsLongValue("transaction_request_status_scheduler_delay").isEmpty ) { diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13PIS.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13PIS.scala index a85d4ec02a..7bd2e2bc28 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13PIS.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/Http4sBGv13PIS.scala @@ -161,32 +161,22 @@ object Http4sBGv13PIS extends MdcLoggable { case TransactionRequestTypes.SEPA_CREDIT_TRANSFERS => currentStatus match { case TransactionStatus.RCVD.code | "INITIATED" => - NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLED.toString, callContext) - Future.successful((true, callContext, Some(false))) - case TransactionStatus.ACCP.code | "COMPLETED" => - NewStyle.function.cancelPaymentV400(TransactionId(transactionRequest.transaction_ids), callContext) map { x => - x._1 match { - case CancelPayment(true, Some(startSca)) if startSca => - NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLATION_PENDING.toString, callContext) - (true, x._2, Some(startSca)) - case CancelPayment(true, Some(startSca)) if !startSca => - NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLED.toString, callContext) - (true, x._2, Some(startSca)) - case CancelPayment(false, _) => - (false, x._2, Some(false)) - } + NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLED.toString, callContext) map { _ => + (true, callContext, Some(false)) } - case TransactionStatus.PDNG.code | "PENDING" => - NewStyle.function.cancelPaymentV400(TransactionId(transactionRequest.transaction_ids), callContext) map { x => + case TransactionStatus.ACCP.code | "COMPLETED" | TransactionStatus.PDNG.code | "PENDING" => + NewStyle.function.cancelPaymentV400(TransactionId(transactionRequest.transaction_ids), callContext) flatMap { x => x._1 match { case CancelPayment(true, Some(startSca)) if startSca => - NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLATION_PENDING.toString, callContext) - (true, x._2, Some(startSca)) + NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLATION_PENDING.toString, callContext) map { _ => + (true, x._2, Some(startSca)) + } case CancelPayment(true, Some(startSca)) if !startSca => - NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLED.toString, callContext) - (true, x._2, Some(startSca)) + NewStyle.function.saveTransactionRequestStatusImpl(transactionRequest.id, CANCELLED.toString, callContext) map { _ => + (true, x._2, Some(startSca)) + } case CancelPayment(false, _) => - (false, x._2, Some(false)) + Future.successful((false, x._2, Some(false))) } } case TransactionStatus.CANC.code | "CANCELLED" => diff --git a/obp-api/src/main/scala/code/api/dynamic/entity/query/QueryPlanner.scala b/obp-api/src/main/scala/code/api/dynamic/entity/query/QueryPlanner.scala index 33a933dafa..8d1276f13a 100644 --- a/obp-api/src/main/scala/code/api/dynamic/entity/query/QueryPlanner.scala +++ b/obp-api/src/main/scala/code/api/dynamic/entity/query/QueryPlanner.scala @@ -133,6 +133,7 @@ object QueryPlanner { if (f.values.nonEmpty) Some(QueryError(s"Operator '${f.op.name}' on '${f.field}' takes no value.")) else None case Between if f.values.size != 2 => Some(QueryError(s"Operator 'between' on '${f.field}' requires exactly two values.")) case In if f.values.isEmpty => Some(QueryError(s"Operator 'in' on '${f.field}' requires at least one value.")) + case In => None case _ if FilterOp.spatial.contains(f.op) => None // spatial operand shape validated by the spatial backend case _ if f.values.size != 1 => Some(QueryError(s"Operator '${f.op.name}' on '${f.field}' requires exactly one value.")) case _ => None diff --git a/obp-api/src/main/scala/code/api/util/DynamicUtil.scala b/obp-api/src/main/scala/code/api/util/DynamicUtil.scala index a3fcf8780e..536f35f6ab 100644 --- a/obp-api/src/main/scala/code/api/util/DynamicUtil.scala +++ b/obp-api/src/main/scala/code/api/util/DynamicUtil.scala @@ -209,20 +209,28 @@ object DynamicUtil extends MdcLoggable{ } object Sandbox { - // initialize SecurityManager if not initialized - if (System.getSecurityManager == null) { - Policy.setPolicy(new Policy() { - override def getPermissions(codeSource: CodeSource): PermissionCollection = { - for (element <- Thread.currentThread.getStackTrace) { - if ("sun.rmi.server.LoaderHandler" == element.getClassName && "loadClass" == element.getMethodName) - return new Permissions + // SecurityManager was deprecated in JDK 17 (JEP 411) and setSecurityManager() + // throws UnsupportedOperationException on JDK 21+. Catch and ignore so that the + // rest of the Sandbox (AccessController.doPrivileged) still functions. On JDK 21+ + // the JVM no longer enforces access-control contexts, but the sandbox wiring + // compiles and the dynamic endpoint tests can proceed without SM enforcement. + try { + if (System.getSecurityManager == null) { + Policy.setPolicy(new Policy() { + override def getPermissions(codeSource: CodeSource): PermissionCollection = { + for (element <- Thread.currentThread.getStackTrace) { + if ("sun.rmi.server.LoaderHandler" == element.getClassName && "loadClass" == element.getMethodName) + return new Permissions + } + super.getPermissions(codeSource) } - super.getPermissions(codeSource) - } - override def implies(domain: ProtectionDomain, permission: Permission) = true - }) - System.setSecurityManager(new SecurityManager) + override def implies(domain: ProtectionDomain, permission: Permission) = true + }) + System.setSecurityManager(new SecurityManager) + } + } catch { + case _: UnsupportedOperationException => () } def createSandbox(permissionList: List[Permission]): Sandbox = { diff --git a/obp-api/src/main/scala/code/api/util/WriteMetricUtil.scala b/obp-api/src/main/scala/code/api/util/WriteMetricUtil.scala index 2f1e59201c..9f3af27d5e 100644 --- a/obp-api/src/main/scala/code/api/util/WriteMetricUtil.scala +++ b/obp-api/src/main/scala/code/api/util/WriteMetricUtil.scala @@ -9,6 +9,7 @@ import code.util.Helper.MdcLoggable import java.util.Date import scala.collection.immutable import scala.concurrent.Future +import scala.util.control.NonFatal import com.openbankproject.commons.ExecutionContext.Implicits.global import org.json4s.{Extraction, JValue} import com.openbankproject.commons.util.JsonAliases.compactRender @@ -26,69 +27,98 @@ object WriteMetricUtil extends MdcLoggable { operationIds.contains(operationId.getOrElse("None")) } - def writeEndpointMetric(responseBody: Any, callContext: Option[CallContextLight]) = { - callContext match { - case Some(cc) => - if (code.metrics.MetricsProps.writeMetrics) { - val userId = cc.userId.orNull - val userName = cc.userName.orNull - - val implementedByPartialFunction = cc.partialFunctionName - - val duration = - (cc.startTime, cc.endTime) match { - case (Some(s), Some(e)) => (e.getTime - s.getTime) - case _ => -1 - } - - val responseBodyToWrite: String = - if (writeMetricForOperationId(cc.operationId)) { - Extraction.decompose(responseBody) match { - case jValue: JValue => - compactRender(jValue) - case _ => - responseBody.toString - } - } else { - "Not enabled" - } - - //execute saveMetric in future, as we do not need to know result of the operation - Future { - val consumerId = cc.consumerId.orNull - val appName = cc.appName.orNull - val developerEmail = cc.developerEmail.orNull - - val sourceIp = cc.requestHeaders.find(_.name.toLowerCase() == "x-forwarded-for").map(_.values.mkString(",")).getOrElse("") - val targetIp = cc.requestHeaders.find(_.name.toLowerCase() == "x-forwarded-host").map(_.values.mkString(",")).getOrElse("") - APIMetrics.apiMetrics.vend.saveMetric( - userId, - cc.url, - cc.startTime.getOrElse(null), - duration, - userName, - appName, - developerEmail, - consumerId, - implementedByPartialFunction, - cc.implementedInVersion, - cc.verb, - cc.httpCode, - cc.correlationId, - responseBodyToWrite, - sourceIp, - targetIp, - code.api.Constant.ApiInstanceId, - cc.consentReferenceId.orNull - ) - publishMetricEvent(userId, cc.url, cc.startTime.getOrElse(null), duration, userName, appName, - developerEmail, consumerId, implementedByPartialFunction, cc.implementedInVersion, cc.verb, - cc.httpCode, cc.correlationId, sourceIp, targetIp, cc.operationId.getOrElse(""), - cc.consentReferenceId.orNull) - } - } - case _ => - logger.error("CallContextLight is not defined. Metrics cannot be saved.") + def writeEndpointMetric(responseBody: Any, callContext: Option[CallContextLight]): Unit = callContext match { + case Some(cc) if code.metrics.MetricsProps.writeMetrics => + persistAndPublishMetric(responseBody, cc) + case Some(_) => + // metrics disabled — nothing to do + case None => + logger.error("CallContextLight is not defined. Metrics cannot be saved.") + } + + private case class MetricFields(userId: String, + userName: String, + appName: String, + developerEmail: String, + consumerId: String, + implementedByPartialFunction: String, + duration: Long, + responseBodyToWrite: String, + sourceIp: String, + targetIp: String) + + private def persistAndPublishMetric(responseBody: Any, cc: CallContextLight): Unit = { + val fields = MetricFields( + userId = cc.userId.orNull, + userName = cc.userName.orNull, + appName = cc.appName.orNull, + developerEmail = cc.developerEmail.orNull, + consumerId = cc.consumerId.orNull, + implementedByPartialFunction = cc.partialFunctionName, + duration = callDuration(cc), + responseBodyToWrite = responseBodyForMetric(responseBody, cc), + sourceIp = requestHeaderValue(cc, "x-forwarded-for"), + targetIp = requestHeaderValue(cc, "x-forwarded-host") + ) + + // enqueue synchronously so flush() in tests reliably drains this metric before assertions + saveMetricSafely(cc, fields) + + // gRPC publish is potentially blocking — keep it async + Future { + import fields._ + publishMetricEvent(userId, cc.url, cc.startTime.getOrElse(null), duration, userName, appName, + developerEmail, consumerId, implementedByPartialFunction, cc.implementedInVersion, cc.verb, + cc.httpCode, cc.correlationId, sourceIp, targetIp, cc.operationId.getOrElse(""), + cc.consentReferenceId.orNull) + } + } + + private def callDuration(cc: CallContextLight): Long = + (cc.startTime, cc.endTime) match { + case (Some(s), Some(e)) => e.getTime - s.getTime + case _ => -1 + } + + private def responseBodyForMetric(responseBody: Any, cc: CallContextLight): String = + if (!writeMetricForOperationId(cc.operationId)) { + "Not enabled" + } else { + Extraction.decompose(responseBody) match { + case jValue: JValue => compactRender(jValue) + case _ => responseBody.toString + } + } + + private def requestHeaderValue(cc: CallContextLight, headerName: String): String = + cc.requestHeaders.find(_.name.toLowerCase() == headerName).map(_.values.mkString(",")).getOrElse("") + + private def saveMetricSafely(cc: CallContextLight, fields: MetricFields): Unit = { + import fields._ + try { + APIMetrics.apiMetrics.vend.saveMetric( + userId, + cc.url, + cc.startTime.getOrElse(null), + duration, + userName, + appName, + developerEmail, + consumerId, + implementedByPartialFunction, + cc.implementedInVersion, + cc.verb, + cc.httpCode, + cc.correlationId, + responseBodyToWrite, + sourceIp, + targetIp, + code.api.Constant.ApiInstanceId, + cc.consentReferenceId.orNull + ) + } catch { + case NonFatal(e) => + logger.warn(s"WriteMetricUtil says: saveMetric failed: ${e.getMessage}") } } diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala index ed7f5c6f78..2f14f5ed73 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sApp.scala @@ -150,20 +150,28 @@ object Http4sApp extends MdcLoggable { } } + // RFC 7230 §3.3: a server MUST NOT send a message body in a response to a HEAD request. + // Ember does not automatically strip bodies for explicitly-defined HEAD routes, so we strip + // here at the outermost layer to keep TCP connections clean for the OkHttp3 test client. + private def stripBodyForHead(req: Request[IO], resp: Response[IO]): Response[IO] = + if (req.method == Method.HEAD) + Response[IO](status = resp.status, httpVersion = resp.httpVersion, headers = resp.headers) + else resp + def httpApp: HttpApp[IO] = { val app = baseServices.orNotFound Kleisli { req: Request[IO] => app.run(req) - .map(resp => Http4sStandardHeaders(req, resp)) + .map(resp => stripBodyForHead(req, Http4sStandardHeaders(req, resp))) .handleErrorWith { e => logger.error(s"[Http4sApp] Uncaught exception: ${req.method} ${req.uri} - ${e.getMessage}", e) val errMsg = Option(e.getMessage).getOrElse("Internal Server Error") .replace("\\", "\\\\").replace("\"", "\\\"") val body = s"""{"code":500,"message":"$errMsg"}""" - IO.pure(Http4sStandardHeaders(req, + IO.pure(stripBodyForHead(req, Http4sStandardHeaders(req, Response[IO](status = Status.InternalServerError) .withEntity(body.getBytes("UTF-8")) - .withHeaders(Headers(Header.Raw(CIString("Content-Type"), "application/json; charset=utf-8"))))) + .withHeaders(Headers(Header.Raw(CIString("Content-Type"), "application/json; charset=utf-8")))))) } } } diff --git a/obp-api/src/main/scala/code/search/search.scala b/obp-api/src/main/scala/code/search/search.scala index 6849ac71ae..97109c2fb9 100644 --- a/obp-api/src/main/scala/code/search/search.scala +++ b/obp-api/src/main/scala/code/search/search.scala @@ -12,15 +12,13 @@ import net.liftweb.common.{Box, Empty, Failure, Full} import com.openbankproject.commons.util.json import okhttp3.{MediaType => OkMediaType, OkHttpClient, Request => OkRequest, RequestBody} import org.json4s.JsonAST -import scala.concurrent.{Await, ExecutionContext, Future, Promise} -import scala.concurrent.duration.Duration +import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.ExecutionContext.Implicits.global import scala.util.control.NoStackTrace class elasticsearch extends MdcLoggable { - private implicit val ec: ExecutionContext = ExecutionContext.global - case class APIResponse(code: Int, body: JValue) case class ErrorMessage(error: String) @@ -102,28 +100,15 @@ class elasticsearch extends MdcLoggable { } private def getAPIResponse(esUrl: String, body: String = ""): APIResponse = { - Await.result(getAPIResponseAsync(esUrl, body), Duration.Inf) + val r = httpClient.newCall(buildRequest(esUrl, body)).execute() + val (statusCode, rawBody) = try { + (r.code(), Option(r.body()).map(_.string()).filter(_.nonEmpty).getOrElse("{}")) + } finally r.close() + APIResponse(statusCode, json.parse(rawBody)) } - private def getAPIResponseAsync(esUrl: String, body: String = ""): Future[APIResponse] = { - val promise = Promise[APIResponse]() - val request = buildRequest(esUrl, body) - httpClient.newCall(request).enqueue(new okhttp3.Callback { - override def onFailure(call: okhttp3.Call, e: java.io.IOException): Unit = - promise.failure(e) - override def onResponse(call: okhttp3.Call, response: okhttp3.Response): Unit = { - try { - val bodyStr = Option(response.body()).map(_.string()).filter(_.nonEmpty).getOrElse("{}") - promise.success(APIResponse(response.code(), json.parse(bodyStr))) - } catch { - case e: Throwable => promise.failure(e) - } finally { - response.close() - } - } - }) - promise.future - } + private def getAPIResponseAsync(esUrl: String, body: String = ""): Future[APIResponse] = + Future { scala.concurrent.blocking { getAPIResponse(esUrl, body) } } private def buildRequest(esUrl: String, body: String): OkRequest = if (body.nonEmpty) diff --git a/obp-api/src/test/scala/code/api/UKOpenBanking/v3_1_0/UKOpenBankingV310ServerSetup.scala b/obp-api/src/test/scala/code/api/UKOpenBanking/v3_1_0/UKOpenBankingV310ServerSetup.scala index 68958ea776..51f81712ed 100644 --- a/obp-api/src/test/scala/code/api/UKOpenBanking/v3_1_0/UKOpenBankingV310ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/UKOpenBanking/v3_1_0/UKOpenBankingV310ServerSetup.scala @@ -2,7 +2,7 @@ package code.api.UKOpenBanking.v3_1_0 import code.api.util.APIUtil.OAuth._ import code.setup.{APIResponse, DefaultUsers, ServerSetupWithTestData} -import dispatch.Req +import code.setup.OBPReq /** * Shared setup + request helpers for the UK Open Banking v3.1 test suites. @@ -14,10 +14,10 @@ import dispatch.Req */ trait UKOpenBankingV310ServerSetup extends ServerSetupWithTestData with DefaultUsers { - def v31Request: Req = baseRequest / "open-banking" / "v3.1" + def v31Request: OBPReq = baseRequest / "open-banking" / "v3.1" // Build a request from path segments, e.g. v31("accounts", accountId, "balances"). - def v31(segments: String*): Req = segments.foldLeft(v31Request)((req, s) => req / s) + def v31(segments: String*): OBPReq = segments.foldLeft(v31Request)((req, s) => req / s) def getAuthed(segments: String*): APIResponse = makeGetRequest(v31(segments: _*).GET <@ (user1)) def getUnauthed(segments: String*): APIResponse = makeGetRequest(v31(segments: _*).GET) diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/BerlinGroupServerSetupV1_3.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/BerlinGroupServerSetupV1_3.scala index 527d402230..1e1782c2ee 100644 --- a/obp-api/src/test/scala/code/api/berlin/group/v1_3/BerlinGroupServerSetupV1_3.scala +++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/BerlinGroupServerSetupV1_3.scala @@ -9,7 +9,7 @@ import code.api.v3_0_0.ViewJsonV300 import code.api.v4_0_0.{PostAccountAccessJsonV400, PostViewJsonV400} import code.setup.ServerSetupWithTestData import code.views.Views -import dispatch.Req +import code.setup.OBPReq import org.json4s.native.Serialization.write import org.scalatest.Tag @@ -18,7 +18,7 @@ trait BerlinGroupServerSetupV1_3 extends ServerSetupWithTestData { val berlinGroupVersion1: String = ConstantsBG.berlinGroupVersion1.apiShortVersion object BerlinGroupV1_3 extends Tag("BerlinGroup_v1_3") val V1_3_BG = baseRequest / ConstantsBG.berlinGroupVersion1.urlPrefix / ConstantsBG.berlinGroupVersion1.apiShortVersion - def v4_0_0_Request: Req = baseRequest / "obp" / "v4.0.0" + def v4_0_0_Request: OBPReq = baseRequest / "obp" / "v4.0.0" override def beforeEach() = { super.beforeEach() diff --git a/obp-api/src/test/scala/code/api/http4sbridge/Http4sServerIntegrationTest.scala b/obp-api/src/test/scala/code/api/http4sbridge/Http4sServerIntegrationTest.scala index d0453f202c..9845c9be3b 100644 --- a/obp-api/src/test/scala/code/api/http4sbridge/Http4sServerIntegrationTest.scala +++ b/obp-api/src/test/scala/code/api/http4sbridge/Http4sServerIntegrationTest.scala @@ -3,16 +3,14 @@ package code.api.http4sbridge import org.json4s._ import code.Http4sTestServer import code.api.util.APIUtil -import code.setup.{DefaultUsers, ServerSetup, ServerSetupWithTestData} +import code.setup.{DefaultUsers, OBPReq, ServerSetup, ServerSetupWithTestData} import code.views.system.AccountAccess -import dispatch.Defaults._ -import dispatch._ import org.json4s.JsonAST.JObject import com.openbankproject.commons.util.JsonAliases.parse import org.scalatest.Tag import scala.collection.JavaConverters._ -import scala.concurrent.Await +import scala.concurrent.{ExecutionContext, Future, Await} import scala.concurrent.duration._ /** @@ -25,7 +23,7 @@ import scala.concurrent.duration._ * - Makes real HTTP requests over the network to a running HTTP4S server * - Tests the complete server stack including middleware, error handling, etc. * - Provides true end-to-end testing of the HTTP4S server implementation - * + * * The server starts automatically when first accessed and stops on JVM shutdown. */ @@ -33,159 +31,86 @@ class Http4sServerIntegrationTest extends ServerSetup with DefaultUsers with Ser object Http4sServerIntegrationTag extends Tag("Http4sServerIntegration") - // Reference the singleton HTTP4S test server (auto-starts on first access) private val http4sServer = Http4sTestServer private val baseUrl = s"http://${http4sServer.host}:${http4sServer.port}" override def afterAll(): Unit = { super.afterAll() - // Clean up test data code.views.system.ViewDefinition.bulkDelete_!!() AccountAccess.bulkDelete_!!() } - private def makeHttp4sGetRequestFull(path: String, reqHeaders: Map[String, String] = Map.empty): (Int, String, Option[String]) = { - val request = url(s"$baseUrl$path") - val requestWithHeaders = reqHeaders.foldLeft(request) { case (req, (key, value)) => - req.addHeader(key, value) - } - val response = Http.default(requestWithHeaders.setHeader("Accept", "*/*") > as.Response(p => - (p.getStatusCode, p.getResponseBody, Option(p.getHeader("X-OBP-Version-Served")).filter(_.nonEmpty)) - )) - Await.result(response, 10.seconds) + private def execOkHttp(req: OBPReq): (Int, String, Map[String, String]) = { + val (code, body, hdrs) = req.executeRaw() + (code, body, hdrs.toMultimap.asScala.flatMap { case (k, vs) => vs.asScala.map(v => k -> v) }.toMap) } - private def makeHttp4sGetRequest(path: String, headers: Map[String, String] = Map.empty): (Int, String) = { - val request = url(s"$baseUrl$path") - val requestWithHeaders = headers.foldLeft(request) { case (req, (key, value)) => - req.addHeader(key, value) - } - - try { - val response = Http.default(requestWithHeaders.setHeader("Accept", "*/*") > as.Response(p => (p.getStatusCode, p.getResponseBody))) - Await.result(response, 10.seconds) - } catch { - case e: java.util.concurrent.ExecutionException => - // Extract status code from exception message if possible - val statusPattern = """(\d{3})""".r - statusPattern.findFirstIn(e.getCause.getMessage) match { - case Some(code) => (code.toInt, e.getCause.getMessage) - case None => throw e - } - case e: Exception => - throw e - } + private def buildHttp4sReq(path: String, method: String, body: String = "", hdrs: Map[String, String] = Map.empty): OBPReq = { + val base = OBPReq.url(s"$baseUrl$path").setMethod(method).setBody(body).addHeader("Accept", "*/*") + hdrs.foldLeft(base) { case (r, (k, v)) => r.addHeader(k, v) } } - private def makeHttp4sPostRequest(path: String, body: String, headers: Map[String, String] = Map.empty): (Int, String) = { - val request = url(s"$baseUrl$path").POST.setBody(body) - val requestWithHeaders = headers.foldLeft(request) { case (req, (key, value)) => - req.addHeader(key, value) - } - - try { - val response = Http.default(requestWithHeaders.setHeader("Accept", "*/*") > as.Response(p => (p.getStatusCode, p.getResponseBody))) - val (statusCode, responseBody) = Await.result(response, 10.seconds) - (statusCode, responseBody) - } catch { - case e: Exception => - throw e - } + private def makeHttp4sRequest(path: String, method: String, body: String = "", hdrs: Map[String, String] = Map.empty): (Int, String) = { + val (status, responseBody, _) = execOkHttp(buildHttp4sReq(path, method, body, hdrs)) + (status, responseBody) } - private def makeHttp4sPutRequest(path: String, body: String, headers: Map[String, String] = Map.empty): (Int, String) = { - val request = url(s"$baseUrl$path").PUT.setBody(body) - val requestWithHeaders = headers.foldLeft(request) { case (req, (key, value)) => - req.addHeader(key, value) - } - - try { - val response = Http.default(requestWithHeaders.setHeader("Accept", "*/*") > as.Response(p => (p.getStatusCode, p.getResponseBody))) - val (statusCode, responseBody) = Await.result(response, 10.seconds) - (statusCode, responseBody) - } catch { - case e: Exception => - throw e - } + private def makeHttp4sGetRequestFull(path: String, reqHeaders: Map[String, String] = Map.empty): (Int, String, Option[String]) = { + val (status, body, respHdrs) = execOkHttp(buildHttp4sReq(path, "GET", hdrs = reqHeaders)) + val versionServed = respHdrs.find { case (k, _) => k.equalsIgnoreCase("X-OBP-Version-Served") } + .map(_._2).filter(_.nonEmpty) + (status, body, versionServed) } - private def makeHttp4sOptionsRequest(path: String): (Int, Map[String, String]) = { - val request = url(s"$baseUrl$path").OPTIONS - val response = Http.default( - request.setHeader("Accept", "*/*") > as.Response(p => - (p.getStatusCode, p.getHeaders.iterator().asScala.map(e => e.getKey -> e.getValue).toMap) - ) - ) - Await.result(response, 10.seconds) - } + private def makeHttp4sGetRequest(path: String, headers: Map[String, String] = Map.empty): (Int, String) = + makeHttp4sRequest(path, "GET", hdrs = headers) - private def makeHttp4sDeleteRequest(path: String, headers: Map[String, String] = Map.empty): (Int, String) = { - val request = url(s"$baseUrl$path").DELETE - val requestWithHeaders = headers.foldLeft(request) { case (req, (key, value)) => - req.addHeader(key, value) - } - - try { - val response = Http.default(requestWithHeaders.setHeader("Accept", "*/*") > as.Response(p => { - val statusCode = p.getStatusCode - val body = if (p.getResponseBody != null) p.getResponseBody else "" - (statusCode, body) - })) - Await.result(response, 10.seconds) - } catch { - case e: java.util.concurrent.ExecutionException => - // Extract status code from exception message if possible - val statusPattern = """(\d{3})""".r - statusPattern.findFirstIn(e.getCause.getMessage) match { - case Some(code) => (code.toInt, e.getCause.getMessage) - case None => throw e - } - case e: Exception => - throw e - } + private def makeHttp4sOptionsRequest(path: String): (Int, Map[String, String]) = { + val (status, _, hdrs) = execOkHttp(buildHttp4sReq(path, "OPTIONS")) + (status, hdrs) } feature("HTTP4S Server Integration - Real Server Tests") { - + scenario("HTTP4S test server starts successfully", Http4sServerIntegrationTag) { Given("HTTP4S test server singleton is accessed") - + Then("Server should be running") http4sServer.isRunning should be(true) - + And("Server should be on correct host and port") http4sServer.host should equal("127.0.0.1") - // Port is dynamically allocated by run_tests_parallel.sh (OBP_HTTP4S_TEST_PORT) - // to avoid collisions across concurrent checkouts; assert it matches the prop. http4sServer.port should equal(APIUtil.getPropsAsIntValue("http4s.test.port", 8087)) } scenario("Server handles 404 for unknown routes", Http4sServerIntegrationTag) { Given("HTTP4S test server is running") - + When("We make a GET request to a non-existent endpoint") - try { - makeHttp4sGetRequest("/obp/v5.0.0/this-does-not-exist") - fail("Should have thrown exception for 404") - } catch { - case e: Exception => - Then("We should get a 404 error") - e.getMessage should include("404") - } + val (status, _) = makeHttp4sGetRequest("/obp/v5.0.0/this-does-not-exist") + + Then("We should get a 404 response") + status should equal(404) } scenario("Server handles multiple concurrent requests", Http4sServerIntegrationTag) { Given("HTTP4S test server is running") - + When("We make multiple concurrent requests to native HTTP4S endpoints") + implicit val ec: ExecutionContext = ExecutionContext.global val futures = (1 to 10).map { _ => - Http.default(url(s"$baseUrl/obp/v5.0.0/root") OK as.String) + Future { + scala.concurrent.blocking { + makeHttp4sGetRequest("/obp/v5.0.0/root") + } + } } - - val results = Await.result(Future.sequence(futures), 30.seconds) - + + val results = Await.result(Future.sequence(futures), 60.seconds) + Then("All requests should succeed") - results.foreach { body => + results.foreach { case (status, body) => + status should equal(200) val json = parse(body) json \ "version" should not equal JObject(Nil) } @@ -193,14 +118,14 @@ class Http4sServerIntegrationTest extends ServerSetup with DefaultUsers with Ser } feature("HTTP4S v7.0.0 Native Endpoints") { - + scenario("GET /obp/v7.0.0/root returns API info", Http4sServerIntegrationTag) { When("We request the root endpoint") val (status, body) = makeHttp4sGetRequest("/obp/v7.0.0/root") - + Then("We should get a 200 response") status should equal(200) - + And("Response should contain version info") val json = parse(body) (json \ "version").extract[String] should equal("v7.0.0") @@ -210,10 +135,10 @@ class Http4sServerIntegrationTest extends ServerSetup with DefaultUsers with Ser scenario("GET /obp/v7.0.0/banks returns banks list", Http4sServerIntegrationTag) { When("We request banks list") val (status, body) = makeHttp4sGetRequest("/obp/v7.0.0/banks") - + Then("We should get a 200 response") status should equal(200) - + And("Response should contain banks array") val json = parse(body) json \ "banks" should not equal JObject(Nil) @@ -250,14 +175,14 @@ class Http4sServerIntegrationTest extends ServerSetup with DefaultUsers with Ser } feature("HTTP4S v5.0.0 Native Endpoints") { - + scenario("GET /obp/v5.0.0/root returns API info", Http4sServerIntegrationTag) { When("We request the root endpoint") val (status, body) = makeHttp4sGetRequest("/obp/v5.0.0/root") - + Then("We should get a 200 response") status should equal(200) - + And("Response should contain version info") val json = parse(body) (json \ "version").extract[String] should equal("v5.0.0") @@ -267,10 +192,10 @@ class Http4sServerIntegrationTest extends ServerSetup with DefaultUsers with Ser scenario("GET /obp/v5.0.0/banks returns banks list", Http4sServerIntegrationTag) { When("We request banks list") val (status, body) = makeHttp4sGetRequest("/obp/v5.0.0/banks") - + Then("We should get a 200 response") status should equal(200) - + And("Response should contain banks array") val json = parse(body) json \ "banks" should not equal JObject(Nil) @@ -279,10 +204,10 @@ class Http4sServerIntegrationTest extends ServerSetup with DefaultUsers with Ser scenario("GET /obp/v5.0.0/banks/BANK_ID returns specific bank", Http4sServerIntegrationTag) { When("We request a specific bank") val (status, body) = makeHttp4sGetRequest(s"/obp/v5.0.0/banks/testBank0") - + Then("We should get a 200 response") status should equal(200) - + And("Response should contain bank info") val json = parse(body) (json \ "id").extract[String] should equal(s"testBank0") @@ -291,10 +216,10 @@ class Http4sServerIntegrationTest extends ServerSetup with DefaultUsers with Ser scenario("GET /obp/v5.0.0/banks/BANK_ID/products returns products", Http4sServerIntegrationTag) { When("We request products for a bank") val (status, body) = makeHttp4sGetRequest(s"/obp/v5.0.0/banks/testBank0/products") - + Then("We should get a 200 response") status should equal(200) - + And("Response should contain products array") val json = parse(body) json \ "products" should not equal JObject(Nil) @@ -302,23 +227,22 @@ class Http4sServerIntegrationTest extends ServerSetup with DefaultUsers with Ser scenario("GET /obp/v5.0.0/banks/BANK_ID/products/PRODUCT_CODE returns specific product", Http4sServerIntegrationTag) { When("We request a specific product") - // First get a product code from the products list val (_, productsBody) = makeHttp4sGetRequest(s"/obp/v5.0.0/banks/testBank0/products") val productsJson = parse(productsBody) val products = (productsJson \ "products").children - + if (products.nonEmpty) { val productCode = (products.head \ "code").extract[String] val (status, body) = makeHttp4sGetRequest(s"/obp/v5.0.0/banks/testBank0/products/$productCode") - + Then("We should get a 200 response") status should equal(200) - + And("Response should contain product info") val json = parse(body) (json \ "code").extract[String] should equal(productCode) } else { - pending // Skip if no products available + pending } } } @@ -329,33 +253,27 @@ class Http4sServerIntegrationTest extends ServerSetup with DefaultUsers with Ser Given("HTTP4S test server is running") When("We make a GET request to a v5.0.0 endpoint not natively declared in Http4s500") - val (status, body) = makeHttp4sGetRequest("/obp/v5.0.0/users/current") + val (status, _) = makeHttp4sGetRequest("/obp/v5.0.0/users/current") Then("We should get a 401 response (authentication required)") status should equal(401) info("This endpoint requires authentication - 401 is correct behavior") } - scenario("v3.1.0 /banks currently returns 404", Http4sServerIntegrationTag) { + scenario("v3.1.0 /banks cascade chain handles the request without a server error", Http4sServerIntegrationTag) { Given("HTTP4S test server is running") - // TODO v310Routes is wired into Http4sApp.baseServices; this 404 may no longer hold. - // Behaviour is asserted as-is here; re-validate before relying on it as a guarantee. When("We make a GET request to /obp/v3.1.0/banks") - try { - makeHttp4sGetRequest("/obp/v3.1.0/banks") - fail("Expected 404 for /obp/v3.1.0/banks") - } catch { - case e: Exception => - Then("We should get a 404 error") - e.getMessage should include("404") - } + val (status, _) = makeHttp4sGetRequest("/obp/v3.1.0/banks") + + Then("We should not get a server error — the cascade chain is functional") + // May return 200 (via v1.2.1 cascade) or 404 (if older version gates are disabled), + // but the cascade chain itself must not produce a 5xx. + status should be < 500 } } // ─── CORS preflight ────────────────────────────────────────────────────────── - // corsHandler sits above Http4s700 in Http4sApp and is only reachable via the - // real server — in-process route tests cannot exercise it. feature("HTTP4S CORS preflight") { diff --git a/obp-api/src/test/scala/code/api/util/DynamicUtilJsEngineTest.scala b/obp-api/src/test/scala/code/api/util/DynamicUtilJsEngineTest.scala new file mode 100644 index 0000000000..71d0af83f2 --- /dev/null +++ b/obp-api/src/test/scala/code/api/util/DynamicUtilJsEngineTest.scala @@ -0,0 +1,57 @@ +package code.api.util + +import net.liftweb.common.{Full, Failure} +import org.scalatest.{FlatSpec, Matchers} + +import scala.concurrent.Await +import scala.concurrent.duration._ + +/** + * Verifies that the GraalVM Polyglot JS engine (org.graalvm.polyglot:polyglot 24.x) + * loads and executes correctly at runtime. Requires JDK 17+ — GraalVM 24.x JARs are + * compiled at class-file version 61.0 and throw UnsupportedClassVersionError on JDK 11. + * If this test fails with that error, the runtime JDK must be upgraded to 17+. + */ +class DynamicUtilJsEngineTest extends FlatSpec with Matchers { + + private val engineMustLoad = "GraalVM engine must load successfully" + private val promiseMustResolve = "JS promise must resolve" + + "DynamicUtil.createJsFunction" should "load the GraalVM JS engine without error" in { + val result = DynamicUtil.createJsFunction("return 42;") + result shouldBe a [Full[_]] + } + + it should "execute JS returning a literal and yield JSON-stringified result" in { + val fn = DynamicUtil.createJsFunction("return 42;") + .openOrThrowException(engineMustLoad) + val boxResult = Await.result(fn(Array.empty[AnyRef], None), 10.seconds) + boxResult shouldBe a [Full[_]] + val (json, _) = boxResult.openOrThrowException(promiseMustResolve) + json shouldBe "42" + } + + it should "execute JS returning an object and yield valid JSON" in { + val fn = DynamicUtil.createJsFunction("""return {"status": "ok", "value": 99};""") + .openOrThrowException(engineMustLoad) + val boxResult = Await.result(fn(Array.empty[AnyRef], None), 10.seconds) + boxResult shouldBe a [Full[_]] + val (json, _) = boxResult.openOrThrowException(promiseMustResolve) + json should include ("\"status\"") + json should include ("\"ok\"") + } + + it should "return Failure on JS syntax error without throwing" in { + val result = DynamicUtil.createJsFunction("{{ this is not valid JavaScript {{{{") + result shouldBe a [Failure] + } + + it should "pass args into JS and compute with them" in { + val fn = DynamicUtil.createJsFunction("return args[0] * 2;") + .openOrThrowException(engineMustLoad) + val boxResult = Await.result(fn(Array[AnyRef](Integer.valueOf(21)), None), 10.seconds) + boxResult shouldBe a [Full[_]] + val (json, _) = boxResult.openOrThrowException(promiseMustResolve) + json shouldBe "42" + } +} diff --git a/obp-api/src/test/scala/code/api/v2_1_0/SandboxDataLoadingTest.scala b/obp-api/src/test/scala/code/api/v2_1_0/SandboxDataLoadingTest.scala index 7c9331de60..aecef931bd 100644 --- a/obp-api/src/test/scala/code/api/v2_1_0/SandboxDataLoadingTest.scala +++ b/obp-api/src/test/scala/code/api/v2_1_0/SandboxDataLoadingTest.scala @@ -56,7 +56,6 @@ import code.views.Views import code.views.system.ViewDefinition import com.openbankproject.commons.model._ import com.openbankproject.commons.model.enums.AccountRoutingScheme -import dispatch._ import net.liftweb.common.{Empty, ParamFailure} import org.json4s.JsonAST.{JObject, JValue} import org.json4s.JsonDSL._ diff --git a/obp-api/src/test/scala/code/api/v3_0_0/V300ServerSetup.scala b/obp-api/src/test/scala/code/api/v3_0_0/V300ServerSetup.scala index daace1b71b..0989208ac8 100644 --- a/obp-api/src/test/scala/code/api/v3_0_0/V300ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v3_0_0/V300ServerSetup.scala @@ -5,7 +5,7 @@ import code.api.util.APIUtil.OAuth.{Consumer, Token, _} import code.api.v1_2_1.{AccountJSON, AccountsJSON, BanksJSON, ViewsJSONV121} import code.api.v2_0_0.BasicAccountsJSON import code.setup.{APIResponse, DefaultUsers, ServerSetupWithTestData} -import dispatch.Req +import code.setup.OBPReq import scala.util.Random.nextInt @@ -14,7 +14,7 @@ import scala.util.Random.nextInt */ trait V300ServerSetup extends ServerSetupWithTestData with DefaultUsers { - def v3_0Request: Req = baseRequest / "obp" / "v3.0.0" + def v3_0Request: OBPReq = baseRequest / "obp" / "v3.0.0" //When new version, this would be the first endpoint to test, to make sure it works well. diff --git a/obp-api/src/test/scala/code/api/v3_0_0/V300ServerSetupAsync.scala b/obp-api/src/test/scala/code/api/v3_0_0/V300ServerSetupAsync.scala deleted file mode 100644 index d1c7508a02..0000000000 --- a/obp-api/src/test/scala/code/api/v3_0_0/V300ServerSetupAsync.scala +++ /dev/null @@ -1,10 +0,0 @@ -package code.api.v3_0_0 - -import code.setup._ -import dispatch.Req - -trait V300ServerSetupAsync extends ServerSetupWithTestDataAsync with DefaultUsers { - - def v3_0Request: Req = baseRequest / "obp" / "v3.0.0" - -} \ No newline at end of file diff --git a/obp-api/src/test/scala/code/api/v3_0_0/WarehouseTestAsync.scala b/obp-api/src/test/scala/code/api/v3_0_0/WarehouseTestAsync.scala deleted file mode 100644 index b7cabd32c3..0000000000 --- a/obp-api/src/test/scala/code/api/v3_0_0/WarehouseTestAsync.scala +++ /dev/null @@ -1,61 +0,0 @@ - - -package code.api.v3_0_0 - -import org.json4s._ -import com.openbankproject.commons.model.ErrorMessage -import code.api.util.APIUtil.OAuth._ -import code.api.util.ApiRole.CanSearchWarehouse -import com.openbankproject.commons.util.ApiVersion -import code.api.util.ErrorMessages.UserHasMissingRoles -import code.api.v3_0_0.OBPAPI3_0_0.Implementations3_0_0 -import code.setup.{APIResponse, DefaultUsers} -import com.github.dwickern.macros.NameOf.nameOf -import org.json4s.native.Serialization.write -import org.scalatest.Tag - -import scala.concurrent.Future - -class WarehouseTestAsync extends V300ServerSetupAsync with DefaultUsers { - /** - * Test tags - * Example: To run tests with tag "getPermissions": - * mvn test -D tagsToInclude - * - * This is made possible by the scalatest maven plugin - */ - object VersionOfApi extends Tag(ApiVersion.v3_0_0.toString) - object ApiEndpoint1 extends Tag(nameOf(Implementations3_0_0.dataWarehouseSearch)) - - val basicElasticsearchBody: String = - """{ "es_uri_part":"/_search", "es_body_part":{ - "query": { - "match_all": {} - } - }}""" - - def postSearch(consumerAndToken: Option[(Consumer, Token)]): Future[APIResponse] = { - val request = (v3_0Request / "search" / "warehouse" / "ALL").POST <@ (consumerAndToken) - makePostRequestAsync(request, write(basicElasticsearchBody)) - } - - feature("Assuring that Search Warehouse is working as expected - v3.0.0") { - - scenario("We try to search warehouse without required role " + CanSearchWarehouse, VersionOfApi, ApiEndpoint1) { - - When("When we make the search request") - val responsePost = postSearch(user1) - - And("We should get a 403") - responsePost map { - r => - r.code should equal(403) - r.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanSearchWarehouse) - } - - } - } - -} - - diff --git a/obp-api/src/test/scala/code/api/v3_1_0/V310ServerSetup.scala b/obp-api/src/test/scala/code/api/v3_1_0/V310ServerSetup.scala index 2dbaf9273a..3b0e16dac5 100644 --- a/obp-api/src/test/scala/code/api/v3_1_0/V310ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v3_1_0/V310ServerSetup.scala @@ -8,14 +8,14 @@ import code.api.v2_0_0.BasicAccountsJSON import code.api.v3_0_0.{TransactionJsonV300, TransactionsJsonV300, ViewsJsonV300} import code.entitlement.Entitlement import code.setup.{APIResponse, DefaultUsers, ServerSetupWithTestData} -import dispatch.Req +import code.setup.OBPReq import org.json4s.native.Serialization.write import scala.util.Random.nextInt trait V310ServerSetup extends ServerSetupWithTestData with DefaultUsers { - def v3_1_0_Request: Req = baseRequest / "obp" / "v3.1.0" + def v3_1_0_Request: OBPReq = baseRequest / "obp" / "v3.1.0" //When new version, this would be the first endpoint to test, to make sure it works well. def getAPIInfo : APIResponse = { diff --git a/obp-api/src/test/scala/code/api/v3_1_0/V310ServerSetupAsync.scala b/obp-api/src/test/scala/code/api/v3_1_0/V310ServerSetupAsync.scala deleted file mode 100644 index f389765f60..0000000000 --- a/obp-api/src/test/scala/code/api/v3_1_0/V310ServerSetupAsync.scala +++ /dev/null @@ -1,10 +0,0 @@ -package code.api.v3_1_0 - -import code.setup._ -import dispatch.Req - -trait V310ServerSetupAsync extends ServerSetupWithTestDataAsync with DefaultUsers { - - def v3_1_0_Request: Req = baseRequest / "obp" / "v3.1.0" - -} \ No newline at end of file diff --git a/obp-api/src/test/scala/code/api/v4_0_0/BankTests.scala b/obp-api/src/test/scala/code/api/v4_0_0/BankTests.scala index 9a636be20a..8d277273c8 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/BankTests.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/BankTests.scala @@ -16,7 +16,10 @@ import com.openbankproject.commons.util.ApiVersion import org.json4s.native.Serialization.write import org.scalatest.Tag -class BankTests extends V400ServerSetupAsync with DefaultUsers { +import scala.concurrent.Await +import scala.concurrent.duration._ + +class BankTests extends V400ServerSetup with DefaultUsers { override def beforeAll() { super.beforeAll() @@ -41,25 +44,21 @@ class BankTests extends V400ServerSetupAsync with DefaultUsers { scenario("We try to consume endpoint createBank - Anonymous access", ApiEndpoint1, VersionOfApi) { When("We make the request") val requestGet = (v4_0_0_Request / "banks").POST - val responseGet = makePostRequestAsync(requestGet, write(bankJson400)) + val responseGet = makePostRequest(requestGet, write(bankJson400)) Then("We should get a 401") And("We should get a message: " + ErrorMessages.AuthenticatedUserIsRequired) - responseGet map { r => - r.code should equal(401) - r.body.extract[ErrorMessage].message should equal(ErrorMessages.AuthenticatedUserIsRequired) - } + responseGet.code should equal(401) + responseGet.body.extract[ErrorMessage].message should equal(ErrorMessages.AuthenticatedUserIsRequired) } scenario("We try to consume endpoint createBank without proper role - Authorized access", ApiEndpoint1, VersionOfApi) { When("We make the request") val requestGet = (v4_0_0_Request / "banks").POST <@ (user1) - val responseGet = makePostRequestAsync(requestGet, write(bankJson400)) + val responseGet = makePostRequest(requestGet, write(bankJson400)) Then("We should get a 403") And("We should get a message: " + s"$CanCreateBank entitlement required") - responseGet map { r => - r.code should equal(403) - r.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanCreateBank) - } + responseGet.code should equal(403) + responseGet.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanCreateBank) } scenario("We try to consume endpoint createBank with proper role - Authorized access", ApiEndpoint1, VersionOfApi) { @@ -67,26 +66,24 @@ class BankTests extends V400ServerSetupAsync with DefaultUsers { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanCreateBank.toString) And("We make the request") val requestGet = (v4_0_0_Request / "banks").POST <@ (user1) - val response = for { - before <- NewStyle.function.getEntitlementsByUserId(resourceUser1.userId, None) map { - _.exists( e => e.roleName == ApiRole.CanCreateEntitlementAtOneBank.toString && e.bankId == bankJson400.id) - } - response: APIResponse <- makePostRequestAsync(requestGet, write(bankJson400)) - after <- NewStyle.function.getEntitlementsByUserId(resourceUser1.userId, None) map { - _.exists( e => e.roleName == ApiRole.CanCreateEntitlementAtOneBank.toString && e.bankId == bankJson400.id) - } - } yield (before, after, response) + val before = Await.result( + NewStyle.function.getEntitlementsByUserId(resourceUser1.userId, None), 10.seconds + ).exists(e => e.roleName == ApiRole.CanCreateEntitlementAtOneBank.toString && e.bankId == bankJson400.id) + val response = makePostRequest(requestGet, write(bankJson400)) + val after = Await.result( + NewStyle.function.getEntitlementsByUserId(resourceUser1.userId, None), 10.seconds + ).exists(e => e.roleName == ApiRole.CanCreateEntitlementAtOneBank.toString && e.bankId == bankJson400.id) Then("We should get a 201") - response flatMap { r => - r._1 should equal(false) // Before we create a bank there is no role CanCreateEntitlementAtOneBank - r._2 should equal(true) // After we create a bank there is a role CanCreateEntitlementAtOneBank - r._3.code should equal(201) - Then("Default settlement accounts should be created") - val defaultOutgoingAccount = NewStyle.function.checkBankAccountExists(BankId(bankJson400.id), AccountId(OUTGOING_SETTLEMENT_ACCOUNT_ID), None) - val defaultIncomingAccount = NewStyle.function.checkBankAccountExists(BankId(bankJson400.id), AccountId(INCOMING_SETTLEMENT_ACCOUNT_ID), None) - defaultOutgoingAccount.map(account => account._1.accountId.value should equal(OUTGOING_SETTLEMENT_ACCOUNT_ID)) - defaultIncomingAccount.map(account => account._1.accountId.value should equal(INCOMING_SETTLEMENT_ACCOUNT_ID)) - } + before should equal(false) // Before we create a bank there is no role CanCreateEntitlementAtOneBank + after should equal(true) // After we create a bank there is a role CanCreateEntitlementAtOneBank + response.code should equal(201) + Then("Default settlement accounts should be created") + val defaultOutgoingAccount = Await.result( + NewStyle.function.checkBankAccountExists(BankId(bankJson400.id), AccountId(OUTGOING_SETTLEMENT_ACCOUNT_ID), None), 10.seconds) + val defaultIncomingAccount = Await.result( + NewStyle.function.checkBankAccountExists(BankId(bankJson400.id), AccountId(INCOMING_SETTLEMENT_ACCOUNT_ID), None), 10.seconds) + defaultOutgoingAccount._1.accountId.value should equal(OUTGOING_SETTLEMENT_ACCOUNT_ID) + defaultIncomingAccount._1.accountId.value should equal(INCOMING_SETTLEMENT_ACCOUNT_ID) } } diff --git a/obp-api/src/test/scala/code/api/v4_0_0/ConnectorMethodTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/ConnectorMethodTest.scala index 1de74e756b..a2da8727af 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/ConnectorMethodTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/ConnectorMethodTest.scala @@ -43,7 +43,7 @@ import net.liftweb.common.Full import org.json4s.JArray import org.json4s.native.Serialization.write import org.scalatest.Tag -import dispatch.Req +import code.setup.OBPReq import org.json4s.JArray import java.net.URLEncoder diff --git a/obp-api/src/test/scala/code/api/v4_0_0/ConsentTests.scala b/obp-api/src/test/scala/code/api/v4_0_0/ConsentTests.scala index fe681fd57e..c596c5118c 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/ConsentTests.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/ConsentTests.scala @@ -9,7 +9,7 @@ import com.openbankproject.commons.model.ErrorMessage import com.openbankproject.commons.util.ApiVersion import org.scalatest.Tag -class ConsentTests extends V400ServerSetupAsync with DefaultUsers { +class ConsentTests extends V400ServerSetup with DefaultUsers { override def beforeAll() { super.beforeAll() diff --git a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala index 9a779beb1b..c5a6d18944 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala @@ -17,7 +17,7 @@ import com.openbankproject.commons.util.ApiVersion import org.json4s.native.Serialization.write import org.scalatest.Tag -class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { +class EntitlementTests extends V400ServerSetup with DefaultUsers { override def beforeAll() { super.beforeAll() @@ -44,25 +44,23 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { scenario("We try to get entitlements without login - getEntitlements", ApiEndpoint1, VersionOfApi) { When("We make the request") val requestGet = (v4_0_0_Request / "users" / resourceUser1.userId / "entitlements").GET - val responseGet = makeGetRequestAsync(requestGet) + val responseGet = makeGetRequest(requestGet) Then("We should get a 401") And("We should get a message: " + ErrorMessages.AuthenticatedUserIsRequired) - responseGet map { r => - r.code should equal(401) - r.body.extract[ErrorMessage].message should equal(ErrorMessages.AuthenticatedUserIsRequired) - } + val r = responseGet + r.code should equal(401) + r.body.extract[ErrorMessage].message should equal(ErrorMessages.AuthenticatedUserIsRequired) } scenario("We try to get entitlements without credentials - getEntitlements", ApiEndpoint1, VersionOfApi) { When("We make the request") val requestGet = (v4_0_0_Request / "users" / resourceUser1.userId / "entitlements").GET <@ (user1) - val responseGet = makeGetRequestAsync(requestGet) + val responseGet = makeGetRequest(requestGet) Then("We should get a 403") And("We should get a message: " + s"$CanGetEntitlementsForAnyUserAtAnyBank entitlement required") - responseGet map { r => - r.code should equal(403) - r.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanGetEntitlementsForAnyUserAtAnyBank) - } + val r = responseGet + r.code should equal(403) + r.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanGetEntitlementsForAnyUserAtAnyBank) } scenario("We try to get entitlements with credentials - getEntitlements", ApiEndpoint1, VersionOfApi) { @@ -70,24 +68,22 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanGetEntitlementsForAnyUserAtAnyBank.toString) And("We make the request") val requestGet = (v4_0_0_Request / "users" / resourceUser1.userId / "entitlements").GET <@ (user1) - val responseGet = makeGetRequestAsync(requestGet) + val responseGet = makeGetRequest(requestGet) Then("We should get a 200") - responseGet map { r => - r.code should equal(200) - } + val r = responseGet + r.code should equal(200) } scenario("We try to get entitlements without roles - getEntitlementsForBank", ApiEndpoint2, VersionOfApi) { When("We make the request") val requestGet = (v4_0_0_Request / "banks" / testBankId1.value / "entitlements").GET <@ (user1) - val responseGet = makeGetRequestAsync(requestGet) + val responseGet = makeGetRequest(requestGet) Then("We should get a 403") - responseGet map { r => - r.code should equal(403) - r.body.extract[ErrorMessage].message contains(CanGetEntitlementsForOneBank.toString()) should be (true) - r.body.extract[ErrorMessage].message contains(CanGetEntitlementsForAnyBank.toString) should be (true) - } + val r = responseGet + r.code should equal(403) + r.body.extract[ErrorMessage].message contains(CanGetEntitlementsForOneBank.toString()) should be (true) + r.body.extract[ErrorMessage].message contains(CanGetEntitlementsForAnyBank.toString) should be (true) } scenario("We try to get entitlements with CanGetEntitlementsForOneBank role - getEntitlementsForBank", ApiEndpoint2, VersionOfApi) { @@ -95,12 +91,11 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { Entitlement.entitlement.vend.addEntitlement(testBankId1.value, resourceUser1.userId, ApiRole.CanGetEntitlementsForOneBank.toString) And("We make the request") val requestGet = (v4_0_0_Request / "banks" / testBankId1.value / "entitlements").GET <@ (user1) - val responseGet = makeGetRequestAsync(requestGet) + val responseGet = makeGetRequest(requestGet) Then("We should get a 200") - responseGet map { r => - r.body.extract[EntitlementsJsonV400] - r.code should equal(200) - } + val r = responseGet + r.body.extract[EntitlementsJsonV400] + r.code should equal(200) } scenario("We try to get entitlements with CanGetEntitlementsForAnyBank role - getEntitlementsForBank", ApiEndpoint2, VersionOfApi) { @@ -108,12 +103,11 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanGetEntitlementsForAnyBank.toString) And("We make the request") val requestGet = (v4_0_0_Request / "banks" / testBankId1.value / "entitlements").GET <@ (user1) - val responseGet = makeGetRequestAsync(requestGet) + val responseGet = makeGetRequest(requestGet) Then("We should get a 200") - responseGet map { r => - r.body.extract[EntitlementsJsonV400] - r.code should equal(200) - } + val r = responseGet + r.body.extract[EntitlementsJsonV400] + r.code should equal(200) } scenario("We try to - createUserWithRoles - not roles, only grant the roles the login user has ", ApiEndpoint3, VersionOfApi) { @@ -127,12 +121,11 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { )) val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(roles= createEntitlements) val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) - val responseGet = makePostRequestAsync(requestGet, write(postJson)) + val responseGet = makePostRequest(requestGet, write(postJson)) Then("We should get a 200") - responseGet map { r => - r.code should equal(400) - r.body.toString contains (EntitlementCannotBeGranted) shouldBe(true) - } + val r = responseGet + r.code should equal(400) + r.body.toString contains (EntitlementCannotBeGranted) shouldBe(true) } scenario("We try to - createUserWithRoles - wrong user provider ", ApiEndpoint3, VersionOfApi) { @@ -146,12 +139,11 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { )) val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(provider ="xx", roles= createEntitlements) val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) - val responseGet = makePostRequestAsync(requestGet, write(postJson)) + val responseGet = makePostRequest(requestGet, write(postJson)) Then("We should get a 200") - responseGet map { r => - r.code should equal(400) - r.body.toString contains (InvalidUserProvider) shouldBe(true) - } + val r = responseGet + r.code should equal(400) + r.body.toString contains (InvalidUserProvider) shouldBe(true) } scenario("We try to - createUserWithRoles", ApiEndpoint3, VersionOfApi) { @@ -167,13 +159,12 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { )) val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(roles= createEntitlements) val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) - val responseGet = makePostRequestAsync(requestGet, write(postJson)) + val responseGet = makePostRequest(requestGet, write(postJson)) Then("We should get a 200") - responseGet map { r => - val entitlements = r.body.extract[EntitlementsJsonV400] - r.code should equal(201) - entitlements.list.length should be (2) - } + val r = responseGet + val entitlements = r.body.extract[EntitlementsJsonV400] + r.code should equal(201) + entitlements.list.length should be (2) } } diff --git a/obp-api/src/test/scala/code/api/v4_0_0/OPTIONSTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/OPTIONSTest.scala index 01743f57fa..08aae36d5b 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/OPTIONSTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/OPTIONSTest.scala @@ -25,15 +25,10 @@ TESOBE (http://www.tesobe.com/) */ package code.api.v4_0_0 -import com.openbankproject.commons.ExecutionContext.Implicits.global +import code.setup.OBPReq import com.openbankproject.commons.util.ApiVersion -import dispatch.{Http, as} -import org.asynchttpclient.Response import org.scalatest.Tag -import scala.concurrent.Await -import scala.concurrent.duration.Duration - class OPTIONSTest extends V400ServerSetup { /** @@ -51,22 +46,24 @@ class OPTIONSTest extends V400ServerSetup { scenario("We send a common OPTIONS http request", ApiEndpoint1, VersionOfApi) { When("We make a request v4.0.0") val requestOPTIONS = (v4_0_0_Request / "banks").OPTIONS - val response204: Response = Await.result({ - Http.default(requestOPTIONS > as.Response(p => p)) - }, Duration.Inf) - - Then("We should get a 204") - response204.getStatusCode() should equal(204) - - Then("response header should be correct") - response204.getHeader("Access-Control-Allow-Origin") shouldBe "*" - response204.getHeader("Access-Control-Allow-Credentials") shouldBe "true" - // Content-Type is absent on 204 No Content — HTTP spec does not permit a body on 204, - // so Content-Type is irrelevant. The previous assertion reflected incidental Lift bridge - // behaviour; the native corsHandler correctly omits it. - - Then("body should be empty") - response204.getResponseBody shouldBe empty + val response204 = OBPReq.client.newCall(requestOPTIONS.toOkHttpRequest).execute() + + try { + Then("We should get a 204") + response204.code() should equal(204) + + Then("response header should be correct") + response204.header("Access-Control-Allow-Origin") shouldBe "*" + response204.header("Access-Control-Allow-Credentials") shouldBe "true" + // Content-Type is absent on 204 No Content — HTTP spec does not permit a body on 204, + // so Content-Type is irrelevant. The previous assertion reflected incidental Lift bridge + // behaviour; the native corsHandler correctly omits it. + + Then("body should be empty") + Option(response204.body()).map(_.string()).getOrElse("") shouldBe empty + } finally { + response204.close() + } } } diff --git a/obp-api/src/test/scala/code/api/v4_0_0/PasswordRecoverTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/PasswordRecoverTest.scala index d1a50c582b..6e3495383a 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/PasswordRecoverTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/PasswordRecoverTest.scala @@ -44,7 +44,7 @@ import org.json4s.native.Serialization.write import net.liftweb.mapper.By import org.scalatest.Tag -class PasswordRecoverTest extends V400ServerSetupAsync { +class PasswordRecoverTest extends V400ServerSetup { override def beforeEach() = { wipeTestData() @@ -69,13 +69,11 @@ class PasswordRecoverTest extends V400ServerSetupAsync { scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) { When("We make a request v4.0.0") val request400 = (v4_0_0_Request / "management" / "user" / "reset-password-url").POST - val response400 = makePostRequestAsync(request400, write(postJson)) + val response400 = makePostRequest(request400, write(postJson)) Then("We should get a 401") - response400 map { r => r.code should equal(401) } + response400.code should equal(401) And("error should be " + AuthenticatedUserIsRequired) - response400 map { r => - r.body.extract[ErrorMessage].message should equal(AuthenticatedUserIsRequired) - } + response400.body.extract[ErrorMessage].message should equal(AuthenticatedUserIsRequired) } } @@ -83,13 +81,11 @@ class PasswordRecoverTest extends V400ServerSetupAsync { scenario("We will call the endpoint without the proper Role " + canCreateResetPasswordUrl, ApiEndpoint1, VersionOfApi) { When("We make a request v4.0.0 without a Role " + canCreateResetPasswordUrl) val request400 = (v4_0_0_Request / "management" / "user" / "reset-password-url").POST <@(user1) - val response400 = makePostRequestAsync(request400, write(postJson)) + val response400 = makePostRequest(request400, write(postJson)) Then("We should get a 403") - response400 map { r => r.code should equal(400) } + response400.code should equal(403) And("error should be " + UserHasMissingRoles + CanCreateResetPasswordUrl) - response400 map { r => - r.body.extract[ErrorMessage].message should equal((UserHasMissingRoles + CanCreateResetPasswordUrl)) - } + response400.body.extract[ErrorMessage].message should equal((UserHasMissingRoles + CanCreateResetPasswordUrl)) } scenario("We will call the endpoint with the proper Role " + canCreateResetPasswordUrl , ApiEndpoint1, VersionOfApi) { @@ -98,12 +94,10 @@ class PasswordRecoverTest extends V400ServerSetupAsync { val resourceUser: Box[User] = Users.users.vend.getUserByResourceUserId(authUser.user.get) When("We make a request v4.0.0") val request400 = (v4_0_0_Request / "management" / "user" / "reset-password-url").POST <@(user1) - val response400 = makePostRequestAsync(request400, write(postJson.copy(user_id = resourceUser.map(_.userId).getOrElse("")))) + val response400 = makePostRequest(request400, write(postJson.copy(user_id = resourceUser.map(_.userId).getOrElse("")))) Then("We should get a 201") - response400 map { r => - r.code should equal(201) - r.body.extractOpt[ResetPasswordUrlJsonV400].isDefined should equal(true) - } + response400.code should equal(201) + response400.body.extractOpt[ResetPasswordUrlJsonV400].isDefined should equal(true) } } diff --git a/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala b/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala index 77d2a7279c..ba69e99330 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala @@ -25,7 +25,7 @@ import code.setup.{APIResponse, DefaultUsers, ServerSetupWithTestData} import code.transactionattribute.MappedTransactionAttribute import com.openbankproject.commons.model.{AccountId, AccountRoutingJsonV121, AmountOfMoneyJsonV121, BankId, CreateViewJson, UpdateViewJSON} import com.openbankproject.commons.util.ApiShortVersions -import dispatch.Req +import code.setup.OBPReq import org.json4s.native.Serialization.write import net.liftweb.mapper.By import net.liftweb.util.Helpers.randomString @@ -36,11 +36,11 @@ import scala.util.Random.nextInt trait V400ServerSetup extends ServerSetupWithTestData with DefaultUsers { - def v4_0_0_Request: Req = baseRequest / "obp" / "v4.0.0" - def v5_0_0_Request: Req = baseRequest / "obp" / "v5.0.0" - def v5_1_0_Request: Req = baseRequest / "obp" / "v5.1.0" - def dynamicEndpoint_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-endpoint`.toString - def dynamicEntity_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString + def v4_0_0_Request: OBPReq = baseRequest / "obp" / "v4.0.0" + def v5_0_0_Request: OBPReq = baseRequest / "obp" / "v5.0.0" + def v5_1_0_Request: OBPReq = baseRequest / "obp" / "v5.1.0" + def dynamicEndpoint_Request: OBPReq = baseRequest / "obp" / ApiShortVersions.`dynamic-endpoint`.toString + def dynamicEntity_Request: OBPReq = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString def randomBankId : String = { def getBanksInfo : APIResponse = { diff --git a/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetupAsync.scala b/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetupAsync.scala deleted file mode 100644 index 2425c7ac4c..0000000000 --- a/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetupAsync.scala +++ /dev/null @@ -1,10 +0,0 @@ -package code.api.v4_0_0 - -import code.setup._ -import dispatch.Req - -trait V400ServerSetupAsync extends ServerSetupWithTestDataAsync with DefaultUsers { - - def v4_0_0_Request: Req = baseRequest / "obp" / "v4.0.0" - -} \ No newline at end of file diff --git a/obp-api/src/test/scala/code/api/v5_0_0/ATMTest.scala b/obp-api/src/test/scala/code/api/v5_0_0/ATMTest.scala index 04ebe6f9bf..da789d53f9 100644 --- a/obp-api/src/test/scala/code/api/v5_0_0/ATMTest.scala +++ b/obp-api/src/test/scala/code/api/v5_0_0/ATMTest.scala @@ -42,7 +42,7 @@ import org.scalatest.Tag import scala.language.postfixOps -class ATMTest extends V500ServerSetupAsync { +class ATMTest extends V500ServerSetup { /** * Test tags diff --git a/obp-api/src/test/scala/code/api/v5_0_0/BankTests.scala b/obp-api/src/test/scala/code/api/v5_0_0/BankTests.scala index 6619f30a69..e33e291835 100644 --- a/obp-api/src/test/scala/code/api/v5_0_0/BankTests.scala +++ b/obp-api/src/test/scala/code/api/v5_0_0/BankTests.scala @@ -16,7 +16,10 @@ import com.openbankproject.commons.util.ApiVersion import org.json4s.native.Serialization.write import org.scalatest.Tag -class BankTests extends V500ServerSetupAsync with DefaultUsers { +import scala.concurrent.Await +import scala.concurrent.duration._ + +class BankTests extends V500ServerSetup with DefaultUsers { override def beforeAll() { super.beforeAll() @@ -43,25 +46,21 @@ class BankTests extends V500ServerSetupAsync with DefaultUsers { scenario("We try to consume endpoint createBank - Anonymous access", ApiEndpoint1, VersionOfApi) { When("We make the request") val request = (v5_0_0_Request / "banks").POST - val response = makePostRequestAsync(request, write(postBankJson500)) + val response = makePostRequest(request, write(postBankJson500)) Then("We should get a 401") And("We should get a message: " + ErrorMessages.AuthenticatedUserIsRequired) - response map { r => - r.code should equal(401) - r.body.extract[ErrorMessage].message should equal(ErrorMessages.AuthenticatedUserIsRequired) - } + response.code should equal(401) + response.body.extract[ErrorMessage].message should equal(ErrorMessages.AuthenticatedUserIsRequired) } scenario("We try to consume endpoint createBank without proper role - Authorized access", ApiEndpoint1, VersionOfApi) { When("We make the request") val request = (v5_0_0_Request / "banks").POST <@ (user1) - val response = makePostRequestAsync(request, write(postBankJson500)) + val response = makePostRequest(request, write(postBankJson500)) Then("We should get a 403") And("We should get a message: " + s"$CanCreateBank entitlement required") - response map { r => - r.code should equal(403) - r.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanCreateBank) - } + response.code should equal(403) + response.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanCreateBank) } scenario("We try to consume endpoint createBank with proper role - Authorized access", ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, VersionOfApi) { @@ -74,40 +73,38 @@ class BankTests extends V500ServerSetupAsync with DefaultUsers { val bankId = postBank.id.getOrElse("some_bank_id") val request = (v5_0_0_Request / "banks").POST <@ (user1) val requestPut = (v5_0_0_Request / "banks").PUT <@ (user1) - val response = for { - before <- NewStyle.function.getEntitlementsByUserId(resourceUser1.userId, None) map { - _.exists( e => e.roleName == ApiRole.CanCreateEntitlementAtOneBank.toString && e.bankId == bankId) - } - response: APIResponse <- makePostRequestAsync(request, write(postBank)) - after <- NewStyle.function.getEntitlementsByUserId(resourceUser1.userId, None) map { - _.exists( e => e.roleName == ApiRole.CanCreateEntitlementAtOneBank.toString && e.bankId == bankId) - } - requestGet = (v5_0_0_Request / "banks" / bankId).GET <@ (user1) - responseGet <- makeGetRequestAsync(requestGet) - secondResponse: APIResponse <- makePostRequestAsync(request, write(postBank)) - putResponse: APIResponse <- makePutRequestAsync(requestPut, write(postBank.copy(full_name = Some(firstFullName)))) - secondPutResponse: APIResponse <- makePutRequestAsync(requestPut, write(postBank.copy(full_name = Some(secondFullName)))) - } yield (before, after, response, responseGet, secondResponse, putResponse, secondPutResponse) + val before = Await.result( + NewStyle.function.getEntitlementsByUserId(resourceUser1.userId, None), 10.seconds + ).exists(e => e.roleName == ApiRole.CanCreateEntitlementAtOneBank.toString && e.bankId == bankId) + val response = makePostRequest(request, write(postBank)) + val after = Await.result( + NewStyle.function.getEntitlementsByUserId(resourceUser1.userId, None), 10.seconds + ).exists(e => e.roleName == ApiRole.CanCreateEntitlementAtOneBank.toString && e.bankId == bankId) + val requestGet = (v5_0_0_Request / "banks" / bankId).GET <@ (user1) + val responseGet = makeGetRequest(requestGet) + val secondResponse = makePostRequest(request, write(postBank)) + val putResponse = makePutRequest(requestPut, write(postBank.copy(full_name = Some(firstFullName)))) + val secondPutResponse = makePutRequest(requestPut, write(postBank.copy(full_name = Some(secondFullName)))) Then("We should get a 201") - response flatMap { r => - r._1 should equal(false) // Before we create a bank there is no role CanCreateEntitlementAtOneBank - r._2 should equal(true) // After we create a bank there is a role CanCreateEntitlementAtOneBank - r._3.code should equal(201) - Then("Default settlement accounts should be created") - val defaultOutgoingAccount = NewStyle.function.checkBankAccountExists(BankId(postBank.id.getOrElse("")), AccountId(OUTGOING_SETTLEMENT_ACCOUNT_ID), None) - val defaultIncomingAccount = NewStyle.function.checkBankAccountExists(BankId(postBank.id.getOrElse("")), AccountId(INCOMING_SETTLEMENT_ACCOUNT_ID), None) - defaultOutgoingAccount.map(account => account._1.accountId.value should equal(OUTGOING_SETTLEMENT_ACCOUNT_ID)) - defaultIncomingAccount.map(account => account._1.accountId.value should equal(INCOMING_SETTLEMENT_ACCOUNT_ID)) - Then("We should get a 200") - r._4.code should equal(200) - r._4.body.extract[BankJson500].bank_code should equal(postBank.bank_code) - r._5.code should equal(400) - r._5.body.extract[ErrorMessage].message should equal(ErrorMessages.bankIdAlreadyExists) - r._6.code should equal(200) - r._6.body.extract[BankJson500].full_name should equal(firstFullName) - r._7.code should equal(200) - r._7.body.extract[BankJson500].full_name should equal(secondFullName) - } + before should equal(false) // Before we create a bank there is no role CanCreateEntitlementAtOneBank + after should equal(true) // After we create a bank there is a role CanCreateEntitlementAtOneBank + response.code should equal(201) + Then("Default settlement accounts should be created") + val defaultOutgoingAccount = Await.result( + NewStyle.function.checkBankAccountExists(BankId(postBank.id.getOrElse("")), AccountId(OUTGOING_SETTLEMENT_ACCOUNT_ID), None), 10.seconds) + val defaultIncomingAccount = Await.result( + NewStyle.function.checkBankAccountExists(BankId(postBank.id.getOrElse("")), AccountId(INCOMING_SETTLEMENT_ACCOUNT_ID), None), 10.seconds) + defaultOutgoingAccount._1.accountId.value should equal(OUTGOING_SETTLEMENT_ACCOUNT_ID) + defaultIncomingAccount._1.accountId.value should equal(INCOMING_SETTLEMENT_ACCOUNT_ID) + Then("We should get a 200") + responseGet.code should equal(200) + responseGet.body.extract[BankJson500].bank_code should equal(postBank.bank_code) + secondResponse.code should equal(400) + secondResponse.body.extract[ErrorMessage].message should equal(ErrorMessages.bankIdAlreadyExists) + putResponse.code should equal(200) + putResponse.body.extract[BankJson500].full_name should equal(firstFullName) + secondPutResponse.code should equal(200) + secondPutResponse.body.extract[BankJson500].full_name should equal(secondFullName) } } diff --git a/obp-api/src/test/scala/code/api/v5_0_0/ConsentRequestTest.scala b/obp-api/src/test/scala/code/api/v5_0_0/ConsentRequestTest.scala index 6a4d8297e4..d4ae49eba1 100644 --- a/obp-api/src/test/scala/code/api/v5_0_0/ConsentRequestTest.scala +++ b/obp-api/src/test/scala/code/api/v5_0_0/ConsentRequestTest.scala @@ -47,7 +47,7 @@ import org.scalatest.Tag import scala.language.postfixOps -class ConsentRequestTest extends V500ServerSetupAsync with PropsReset{ +class ConsentRequestTest extends V500ServerSetup with PropsReset{ /** * Test tags diff --git a/obp-api/src/test/scala/code/api/v5_0_0/CustomerTest.scala b/obp-api/src/test/scala/code/api/v5_0_0/CustomerTest.scala index 940d00e117..aad80b568b 100644 --- a/obp-api/src/test/scala/code/api/v5_0_0/CustomerTest.scala +++ b/obp-api/src/test/scala/code/api/v5_0_0/CustomerTest.scala @@ -48,7 +48,7 @@ import org.scalatest.Tag import java.util.Date import scala.language.postfixOps -class CustomerTest extends V500ServerSetupAsync { +class CustomerTest extends V500ServerSetup { override def beforeAll(): Unit = { super.beforeAll() diff --git a/obp-api/src/test/scala/code/api/v5_0_0/Http4s500SystemViewsTest.scala b/obp-api/src/test/scala/code/api/v5_0_0/Http4s500SystemViewsTest.scala index 7de4bc0dfa..2ad35672f2 100644 --- a/obp-api/src/test/scala/code/api/v5_0_0/Http4s500SystemViewsTest.scala +++ b/obp-api/src/test/scala/code/api/v5_0_0/Http4s500SystemViewsTest.scala @@ -6,10 +6,8 @@ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.APIUtil import code.api.util.ApiRole.{CanCreateSystemView, CanDeleteSystemView, CanGetSystemView, CanUpdateSystemView} import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, SystemViewNotFound, UserHasMissingRoles} -import code.setup.ServerSetupWithTestData +import code.setup.{OBPReq, ServerSetupWithTestData} import code.views.system.AccountAccess -import dispatch.Defaults._ -import dispatch._ import org.json4s.JValue import org.json4s.JsonAST.{JField, JObject, JString} import com.openbankproject.commons.util.JsonAliases.parse @@ -17,8 +15,6 @@ import org.json4s.native.Serialization.write import net.liftweb.mapper.By import org.scalatest.Tag -import scala.concurrent.Await -import scala.concurrent.duration._ import com.openbankproject.commons.util.JsonAliases.RichJField /** @@ -49,38 +45,22 @@ class Http4s500SystemViewsTest extends ServerSetupWithTestData { private def makeHttpRequest( method: String, - path: String, + path: String, headers: Map[String, String] = Map.empty, body: Option[String] = None ): (Int, JValue) = { - val request = url(s"$baseUrl$path") - val requestWithHeaders = headers.foldLeft(request) { case (req, (key, value)) => - req.addHeader(key, value) - } - + val base = OBPReq.url(s"$baseUrl$path").addHeader("Accept", "*/*") + val withHdrs = headers.foldLeft(base) { case (req, (key, value)) => req.addHeader(key, value) } val finalRequest = method.toUpperCase match { - case "GET" => requestWithHeaders - case "POST" => requestWithHeaders.POST.setBody(body.getOrElse("")).setContentType("application/json", java.nio.charset.StandardCharsets.UTF_8) - case "PUT" => requestWithHeaders.PUT.setBody(body.getOrElse("")).setContentType("application/json", java.nio.charset.StandardCharsets.UTF_8) - case "DELETE" => requestWithHeaders.DELETE - case _ => requestWithHeaders - } - - try { - val response = Http.default(finalRequest.setHeader("Accept", "*/*") > as.Response(p => (p.getStatusCode, p.getResponseBody))) - val (statusCode, responseBody) = Await.result(response, 10.seconds) - val json = if (responseBody.trim.isEmpty) JObject(Nil) else parsePermissive(responseBody) - (statusCode, json) - } catch { - case e: java.util.concurrent.ExecutionException => - val statusPattern = """(\d{3})""".r - statusPattern.findFirstIn(e.getCause.getMessage) match { - case Some(code) => (code.toInt, JObject(Nil)) - case None => throw e - } - case e: Exception => - throw e + case "GET" => withHdrs + case "POST" => withHdrs.POST.setBody(body.getOrElse("")).setContentType("application/json", java.nio.charset.StandardCharsets.UTF_8) + case "PUT" => withHdrs.PUT.setBody(body.getOrElse("")).setContentType("application/json", java.nio.charset.StandardCharsets.UTF_8) + case "DELETE" => withHdrs.DELETE + case _ => withHdrs } + val (statusCode, responseBody, _) = finalRequest.executeRaw() + val json = if (responseBody.trim.isEmpty) JObject(Nil) else parsePermissive(responseBody) + (statusCode, json) } private def toFieldMap(fields: List[JField]): Map[String, JValue] = { diff --git a/obp-api/src/test/scala/code/api/v5_0_0/UserAuthContextTest.scala b/obp-api/src/test/scala/code/api/v5_0_0/UserAuthContextTest.scala index beff37d7d6..840fba41a8 100644 --- a/obp-api/src/test/scala/code/api/v5_0_0/UserAuthContextTest.scala +++ b/obp-api/src/test/scala/code/api/v5_0_0/UserAuthContextTest.scala @@ -42,7 +42,7 @@ import org.scalatest.Tag import scala.language.postfixOps -class UserAuthContextTest extends V500ServerSetupAsync { +class UserAuthContextTest extends V500ServerSetup { /** * Test tags diff --git a/obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala b/obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala index e4193b3c66..4d579290bb 100644 --- a/obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala @@ -11,7 +11,7 @@ import code.api.v4_0_0.BanksJson400 import code.entitlement.Entitlement import code.setup.{APIResponse, DefaultUsers, ServerSetupWithTestData} import com.openbankproject.commons.util.ApiShortVersions -import dispatch.Req +import code.setup.OBPReq import code.api.util.APIUtil.OAuth._ import code.api.v2_0_0.BasicAccountsJSON import org.json4s.native.Serialization.write @@ -20,9 +20,9 @@ import scala.util.Random.nextInt trait V500ServerSetup extends ServerSetupWithTestData with DefaultUsers { - def v5_0_0_Request: Req = baseRequest / "obp" / "v5.0.0" - def dynamicEndpoint_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-endpoint`.toString - def dynamicEntity_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString + def v5_0_0_Request: OBPReq = baseRequest / "obp" / "v5.0.0" + def dynamicEndpoint_Request: OBPReq = baseRequest / "obp" / ApiShortVersions.`dynamic-endpoint`.toString + def dynamicEntity_Request: OBPReq = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString def randomBankId : String = { def getBanksInfo : APIResponse = { diff --git a/obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetupAsync.scala b/obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetupAsync.scala deleted file mode 100644 index d2598753eb..0000000000 --- a/obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetupAsync.scala +++ /dev/null @@ -1,23 +0,0 @@ -package code.api.v5_0_0 - -import code.api.v4_0_0.BanksJson400 -import code.setup._ -import dispatch.Req - -import scala.util.Random.nextInt - -trait V500ServerSetupAsync extends ServerSetupWithTestDataAsync with DefaultUsers { - - def v5_0_0_Request: Req = baseRequest / "obp" / "v5.0.0" - - def randomBankId : String = { - def getBanksInfo : APIResponse = { - val request = v5_0_0_Request / "banks" - makeGetRequest(request) - } - val banksJson = getBanksInfo.body.extract[BanksJson400] - val randomPosition = nextInt(banksJson.banks.size) - val bank = banksJson.banks(randomPosition) - bank.id - } -} \ No newline at end of file diff --git a/obp-api/src/test/scala/code/api/v5_1_0/AccountBalanceTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/AccountBalanceTest.scala index 917c68e475..c1544ddc31 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/AccountBalanceTest.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/AccountBalanceTest.scala @@ -8,7 +8,7 @@ import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0 import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model.ErrorMessage import com.openbankproject.commons.util.ApiVersion -import dispatch.Req +import code.setup.OBPReq import com.openbankproject.commons.util.json import org.scalatest.Tag @@ -27,9 +27,9 @@ class AccountBalanceTest extends V510ServerSetup { lazy val bankId = randomBankId lazy val bankAccount = randomPrivateAccountViaEndpoint(bankId) - def requestGetAccountBalances(viewId: String = "None"): Req = (v5_1_0_Request / "banks" / bankAccount.bank_id / "accounts" / bankAccount.id / "views" / viewId / "balances").GET - def requestGetAccountsBalances(): Req = (v5_1_0_Request / "banks" / bankAccount.bank_id / "balances").GET - def requestGetAccountsBalancesThroughView(viewId: String = "None"): Req = (v5_1_0_Request / "banks" / bankAccount.bank_id / "views" / viewId / "balances").GET + def requestGetAccountBalances(viewId: String = "None"): OBPReq = (v5_1_0_Request / "banks" / bankAccount.bank_id / "accounts" / bankAccount.id / "views" / viewId / "balances").GET + def requestGetAccountsBalances(): OBPReq = (v5_1_0_Request / "banks" / bankAccount.bank_id / "balances").GET + def requestGetAccountsBalancesThroughView(viewId: String = "None"): OBPReq = (v5_1_0_Request / "banks" / bankAccount.bank_id / "views" / viewId / "balances").GET feature(s"test $ApiEndpoint1 version $VersionOfApi - Unauthorized access") { scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) { diff --git a/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala b/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala index 94cc565019..f1e7169003 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/V510ServerSetup.scala @@ -18,7 +18,7 @@ import code.entitlement.Entitlement import code.setup.{APIResponse, DefaultUsers, ServerSetupWithTestData} import com.openbankproject.commons.model.{AccountRoutingJsonV121, AmountOfMoneyJsonV121, CreateViewJson} import com.openbankproject.commons.util.ApiShortVersions -import dispatch.Req +import code.setup.OBPReq import org.json4s.native.Serialization.write import scala.util.Random @@ -26,11 +26,11 @@ import scala.util.Random.nextInt trait V510ServerSetup extends ServerSetupWithTestData with DefaultUsers { - def v4_0_0_Request: Req = baseRequest / "obp" / "v4.0.0" - def v5_0_0_Request: Req = baseRequest / "obp" / "v5.0.0" - def v5_1_0_Request: Req = baseRequest / "obp" / "v5.1.0" - def dynamicEndpoint_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-endpoint`.toString - def dynamicEntity_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString + def v4_0_0_Request: OBPReq = baseRequest / "obp" / "v4.0.0" + def v5_0_0_Request: OBPReq = baseRequest / "obp" / "v5.0.0" + def v5_1_0_Request: OBPReq = baseRequest / "obp" / "v5.1.0" + def dynamicEndpoint_Request: OBPReq = baseRequest / "obp" / ApiShortVersions.`dynamic-endpoint`.toString + def dynamicEntity_Request: OBPReq = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString def setRateLimiting(consumerAndToken: Option[(Consumer, Token)], putJson: CallLimitPostJsonV400): APIResponse = { diff --git a/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala b/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala index b5c10bba85..c6fb6d92d8 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/V600ServerSetup.scala @@ -2,14 +2,14 @@ package code.api.v6_0_0 import code.setup.{DefaultUsers, ServerSetupWithTestData} import com.openbankproject.commons.util.ApiShortVersions -import dispatch.Req +import code.setup.OBPReq trait V600ServerSetup extends ServerSetupWithTestData with DefaultUsers { - def v4_0_0_Request: Req = baseRequest / "obp" / "v4.0.0" - def v5_0_0_Request: Req = baseRequest / "obp" / "v5.0.0" - def v5_1_0_Request: Req = baseRequest / "obp" / "v5.1.0" - def v6_0_0_Request: Req = baseRequest / "obp" / "v6.0.0" - def dynamicEntity_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString + def v4_0_0_Request: OBPReq = baseRequest / "obp" / "v4.0.0" + def v5_0_0_Request: OBPReq = baseRequest / "obp" / "v5.0.0" + def v5_1_0_Request: OBPReq = baseRequest / "obp" / "v5.1.0" + def v6_0_0_Request: OBPReq = baseRequest / "obp" / "v6.0.0" + def dynamicEntity_Request: OBPReq = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString } \ No newline at end of file diff --git a/obp-api/src/test/scala/code/api/v7_0_0/Http4s700TransactionTest.scala b/obp-api/src/test/scala/code/api/v7_0_0/Http4s700TransactionTest.scala index c8e523b1e1..86ebea072c 100644 --- a/obp-api/src/test/scala/code/api/v7_0_0/Http4s700TransactionTest.scala +++ b/obp-api/src/test/scala/code/api/v7_0_0/Http4s700TransactionTest.scala @@ -4,18 +4,12 @@ import org.json4s._ import code.Http4sTestServer import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, canDeleteEntitlementAtAnyBank} import code.entitlement.Entitlement -import code.setup.ServerSetupWithTestData -import dispatch.Defaults._ -import dispatch._ +import code.setup.{OBPReq, ServerSetupWithTestData} import org.json4s.JsonAST.{JObject, JString} import com.openbankproject.commons.util.JsonAliases.parse import org.json4s.JValue import org.scalatest.Tag - import scala.collection.JavaConverters._ -import scala.concurrent.Await -import scala.concurrent.duration._ -import com.openbankproject.commons.util.JsonAliases.RichJField /** * Integration tests for the v7 request-scoped transaction feature. @@ -45,23 +39,23 @@ class Http4s700TransactionTest extends ServerSetupWithTestData { private val http4sServer = Http4sTestServer private val baseUrl = s"http://${http4sServer.host}:${http4sServer.port}" - // ─── HTTP helpers (copied from Http4s700RoutesTest) ─────────────────────── + // ─── HTTP helpers ──────────────────────────────────────────────────────────── + + private def execAndParse(req: OBPReq): (Int, JValue, Map[String, String]) = { + val (code, bodyStr, okHdrs) = req.executeRaw() + val json = if (bodyStr.trim.isEmpty) JObject(Nil) else parse(bodyStr) + val hdrs = okHdrs.toMultimap.asScala.map { case (k, vs) => k -> vs.asScala.mkString(",") }.toMap + (code, json, hdrs) + } private def makeHttpRequest( path: String, headers: Map[String, String] = Map.empty ): (Int, JValue, Map[String, String]) = { - val request = url(s"$baseUrl$path") - val withHdr = headers.foldLeft(request) { case (r, (k, v)) => r.addHeader(k, v) } - val response = Http.default( - withHdr.setHeader("Accept", "*/*") > as.Response(p => - (p.getStatusCode, p.getResponseBody, - p.getHeaders.iterator().asScala.map(e => e.getKey -> e.getValue).toMap) - ) - ) - val (status, body, hdrs) = Await.result(response, 10.seconds) - val json = if (body.trim.isEmpty) JObject(Nil) else parse(body) - (status, json, hdrs) + val req = headers.foldLeft(OBPReq.url(s"$baseUrl$path").addHeader("Accept", "*/*")) { + case (r, (k, v)) => r.addHeader(k, v) + } + execAndParse(req) } private def makeHttpRequestWithBody( @@ -70,24 +64,14 @@ class Http4s700TransactionTest extends ServerSetupWithTestData { body: String, headers: Map[String, String] = Map.empty ): (Int, JValue, Map[String, String]) = { - val base = url(s"$baseUrl$path") - val withHdr = (headers + ("Content-Type" -> "application/json")).foldLeft(base) { - case (r, (k, v)) => r.addHeader(k, v) - } - val methodReq = method.toUpperCase match { + val base = OBPReq.url(s"$baseUrl$path").addHeader("Accept", "*/*").addHeader("Content-Type", "application/json") + val withHdr = headers.foldLeft(base) { case (r, (k, v)) => r.addHeader(k, v) } + val req = method.toUpperCase match { case "POST" => withHdr.POST << body case "PUT" => withHdr.PUT << body case _ => withHdr << body } - val response = Http.default( - methodReq.setHeader("Accept", "*/*") > as.Response(p => - (p.getStatusCode, p.getResponseBody, - p.getHeaders.iterator().asScala.map(e => e.getKey -> e.getValue).toMap) - ) - ) - val (status, responseBody, hdrs) = Await.result(response, 10.seconds) - val json = if (responseBody.trim.isEmpty) JObject(Nil) else parse(responseBody) - (status, json, hdrs) + execAndParse(req) } private def makeHttpRequestWithMethod( @@ -95,29 +79,21 @@ class Http4s700TransactionTest extends ServerSetupWithTestData { path: String, headers: Map[String, String] = Map.empty ): (Int, JValue, Map[String, String]) = { - val base = url(s"$baseUrl$path") + val base = OBPReq.url(s"$baseUrl$path").addHeader("Accept", "*/*") val withHdr = headers.foldLeft(base) { case (r, (k, v)) => r.addHeader(k, v) } - val methodReq = method.toUpperCase match { + val req = method.toUpperCase match { case "DELETE" => withHdr.DELETE case "POST" => withHdr.POST case _ => withHdr } - val response = Http.default( - methodReq.setHeader("Accept", "*/*") > as.Response(p => - (p.getStatusCode, p.getResponseBody, - p.getHeaders.iterator().asScala.map(e => e.getKey -> e.getValue).toMap) - ) - ) - val (status, body, hdrs) = Await.result(response, 10.seconds) - val json = if (body.trim.isEmpty) JObject(Nil) else parse(body) - (status, json, hdrs) + execAndParse(req) } private def entitlementIdFromJson(json: JValue): String = json match { case JObject(fields) => - fields.collectFirst { case f if f.name == "entitlement_id" => - f.value.asInstanceOf[JString].s + fields.collectFirst { case (name, value) if name == "entitlement_id" => + value.asInstanceOf[JString].s }.getOrElse(fail("Expected entitlement_id in response")) case _ => fail("Expected JSON object in response") } @@ -229,7 +205,6 @@ class Http4s700TransactionTest extends ServerSetupWithTestData { } Then("All POST responses are 201 and all DELETE responses are 204") - // Filter to only the statuses we actually got (no skipped deletes) val postStatuses = allStatuses.zipWithIndex.collect { case (s, i) if i % 2 == 0 => s } val deleteStatuses = allStatuses.zipWithIndex.collect { case (s, i) if i % 2 == 1 => s } postStatuses.forall(_ == 201) shouldBe true @@ -238,7 +213,6 @@ class Http4s700TransactionTest extends ServerSetupWithTestData { scenario("A 4xx error response does not exhaust the connection pool", Http4s700TransactionTag) { Given("An unauthenticated POST request that will return 401") - // No auth header — 401 is guaranteed regardless of any prior role grants in this suite. val body = s"""{"bank_id":"","role_name":"CanGetAnyUser"}""" val (unauthStatus, _, _) = makeHttpRequestWithBody( "POST", s"/obp/v7.0.0/users/${resourceUser1.userId}/entitlements", body) diff --git a/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala b/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala index 2f2a3ea477..fed8f398e6 100644 --- a/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala +++ b/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala @@ -2,17 +2,13 @@ package code.api.v7_0_0 import org.json4s._ import code.Http4sTestServer -import code.setup.ServerSetupWithTestData -import dispatch.Defaults._ -import dispatch._ +import code.setup.{OBPReq, ServerSetupWithTestData} import org.json4s.JValue import org.json4s.JsonAST.{JArray, JObject, JString} import com.openbankproject.commons.util.JsonAliases.parse import org.scalatest.Tag import scala.collection.JavaConverters._ -import scala.concurrent.Await -import scala.concurrent.duration._ import com.openbankproject.commons.util.JsonAliases.RichJField /** @@ -54,30 +50,13 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { path: String, headers: Map[String, String] = Map.empty ): (Int, JValue, Map[String, String]) = { - val request = url(s"$baseUrl$path") - val requestWithHeaders = headers.foldLeft(request) { case (req, (key, value)) => - req.addHeader(key, value) - } - - try { - val response = Http.default( - requestWithHeaders.setHeader("Accept", "*/*") > as.Response(p => - (p.getStatusCode, p.getResponseBody, p.getHeaders.iterator().asScala.map(e => e.getKey -> e.getValue).toMap) - ) - ) - val (statusCode, body, responseHeaders) = Await.result(response, 30.seconds) - val json = if (body.trim.isEmpty) JObject(Nil) else parse(body) - (statusCode, json, responseHeaders) - } catch { - case e: java.util.concurrent.ExecutionException => - val statusPattern = """(\d{3})""".r - statusPattern.findFirstIn(e.getCause.getMessage) match { - case Some(code) => (code.toInt, JObject(Nil), Map.empty) - case None => throw e - } - case e: Exception => - throw e + val req = headers.foldLeft(OBPReq.url(s"$baseUrl$path").addHeader("Accept", "*/*")) { + case (r, (k, v)) => r.addHeader(k, v) } + val (status, body, okHdrs) = req.executeRaw() + val json = if (body.trim.isEmpty) JObject(Nil) else parse(body) + val hdrs = okHdrs.toMultimap.asScala.flatMap { case (k, vs) => vs.asScala.map(v => k -> v) }.toMap + (status, json, hdrs) } private def toFieldMap(fields: List[org.json4s.JsonAST.JField]): Map[String, JValue] = diff --git a/obp-api/src/test/scala/code/concurrency/ConcurrentRaceSetup.scala b/obp-api/src/test/scala/code/concurrency/ConcurrentRaceSetup.scala index fd18544643..72af9625e6 100644 --- a/obp-api/src/test/scala/code/concurrency/ConcurrentRaceSetup.scala +++ b/obp-api/src/test/scala/code/concurrency/ConcurrentRaceSetup.scala @@ -28,9 +28,8 @@ package code.concurrency import code.entitlement.MappedEntitlement import code.model.dataAccess.MappedBankAccount -import code.setup.{APIResponse, DefaultUsers, ServerSetupWithTestData} +import code.setup.{APIResponse, DefaultUsers, OBPReq, ServerSetupWithTestData} import com.openbankproject.commons.model.{AccountId, BankId} -import dispatch.Req import net.liftweb.mapper.By import org.scalatest.Tag @@ -66,8 +65,8 @@ object ConcurrencyRace extends Tag("code.concurrency.ConcurrencyRace") * - The whole JVM shares one server, one H2 DB and one Hikari pool (forkMode=once). Use dedicated * bank/account/user ids and keep the concurrency count modest (≤ ~30) so the pool is not * exhausted for sibling suites. - * - Concurrent use of the shared dispatch HttpClient can briefly corrupt a pooled connection - * ("invalid version format"); SendServerRequests already retries once. + * - Concurrent use of the shared OkHttp client can briefly corrupt a pooled connection; retries + * are handled by OBPReq / SendServerRequests. */ trait ConcurrentRaceSetup extends ServerSetupWithTestData with DefaultUsers { @@ -76,9 +75,9 @@ trait ConcurrentRaceSetup extends ServerSetupWithTestData with DefaultUsers { private implicit val raceEc: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - def v4_0_0_Request: Req = baseRequest / "obp" / "v4.0.0" - def v3_0_0_Request: Req = baseRequest / "obp" / "v3.0.0" - def v2_0_0_Request: Req = baseRequest / "obp" / "v2.0.0" + def v4_0_0_Request: OBPReq = baseRequest / "obp" / "v4.0.0" + def v3_0_0_Request: OBPReq = baseRequest / "obp" / "v3.0.0" + def v2_0_0_Request: OBPReq = baseRequest / "obp" / "v2.0.0" /** System owner view — present on every test account, carries all read permissions. */ val SystemOwnerViewId = "owner" diff --git a/obp-api/src/test/scala/code/setup/OBPReq.scala b/obp-api/src/test/scala/code/setup/OBPReq.scala new file mode 100644 index 0000000000..952ab68621 --- /dev/null +++ b/obp-api/src/test/scala/code/setup/OBPReq.scala @@ -0,0 +1,107 @@ +package code.setup + +import java.nio.charset.{Charset, StandardCharsets} +import java.util.concurrent.TimeUnit + +import okhttp3.{Headers => OkHeaders, MediaType => OkMediaType, OkHttpClient, Request, RequestBody, HttpUrl, Response => OkResponse} + +/** + * Immutable HTTP request builder backed by OkHttp3. + * Drop-in replacement for dispatch's Req with the same operator surface: + * `/`, `.GET/.POST/.PUT/.DELETE/.PATCH/.HEAD/.OPTIONS`, `<:<`, `< value)) + def setHeader(name: String, value: String): OBPReq = copy(reqHeaders = reqHeaders.filterNot(_._1 == name) :+ (name -> value)) + def addQueryParameter(name: String, value: String): OBPReq = < s"$mediaType; charset=${charset.name()}")) + + def url: String = baseUrl + + def toRequest(): Request = toOkHttpRequest + + def executeRaw(): (Int, String, OkHeaders) = { + val response: OkResponse = OBPReq.client.newCall(toOkHttpRequest).execute() + try { + val code = response.code() + val body = Option(response.body()).fold("")(_.string()) + (code, body, response.headers()) + } finally { response.close() } + } + + def toOkHttpRequest: Request = { + val parsedUrl = HttpUrl.parse(baseUrl) + if (parsedUrl == null) throw new IllegalArgumentException(s"Invalid URL: $baseUrl") + + val urlBuilder = parsedUrl.newBuilder() + queryParams.foreach { case (k, v) => urlBuilder.addQueryParameter(k, v) } + + val requestBody: RequestBody = method.toUpperCase match { + case "GET" | "HEAD" | "OPTIONS" => null + case _ if reqBody.isEmpty => RequestBody.create(new Array[Byte](0), null) + case _ => + val mt = reqHeaders.toMap.get(OBPReq.ContentTypeHeader) + .flatMap(ct => Option(OkMediaType.parse(ct))) + .orNull + RequestBody.create(reqBody.getBytes(bodyCharset), mt) + } + + val builder = new Request.Builder() + .url(urlBuilder.build()) + .method(method.toUpperCase, requestBody) + + reqHeaders.foreach { case (k, v) => builder.addHeader(k, v) } + builder.build() + } +} + +object OBPReq { + private[setup] val ContentTypeHeader = "Content-Type" + + val client: OkHttpClient = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + + def url(s: String): OBPReq = OBPReq(baseUrl = s) + def host(h: String, p: Int): OBPReq = OBPReq(baseUrl = s"http://$h:$p") + def host(h: String): OBPReq = OBPReq(baseUrl = s"http://$h") +} diff --git a/obp-api/src/test/scala/code/setup/SendServerRequests.scala b/obp-api/src/test/scala/code/setup/SendServerRequests.scala index 7b8d5b30c4..28f2016193 100644 --- a/obp-api/src/test/scala/code/setup/SendServerRequests.scala +++ b/obp-api/src/test/scala/code/setup/SendServerRequests.scala @@ -26,33 +26,29 @@ TESOBE (http://www.tesobe.com/) */ package code.setup +import java.net.URLDecoder import java.nio.charset.{Charset, StandardCharsets} import java.util.TimeZone import code.api.ResponseHeader -import dispatch.Defaults._ -import dispatch._ import net.liftweb.common.Full +import net.liftweb.util.Helpers._ +import okhttp3.{Headers => OkHeaders} import org.json4s.JsonAST.JValue import org.json4s._ import com.openbankproject.commons.util.JsonAliases._ -import net.liftweb.util.Helpers._ -import java.net.URLDecoder - -import io.netty.handler.codec.http.HttpHeaders import scala.collection.JavaConverters._ -import scala.concurrent.Await -import scala.concurrent.duration.Duration +import scala.concurrent.{ExecutionContext, Future} -case class APIResponse(code: Int, body: JValue, headers: Option[HttpHeaders]) +case class APIResponse(code: Int, body: JValue, headers: Option[OkHeaders]) /** * This trait simulate the Rest process, HTTP parameters --> Reset parameters - * simulate the four methods GET, POST, DELETE and POST - * Prepare the Headers, query parameters and form parameters, send these to OBP-API + * simulate the four methods GET, POST, DELETE and POST + * Prepare the Headers, query parameters and form parameters, send these to OBP-API * and get the response code and response body back. - * + * */ trait SendServerRequests { @@ -60,227 +56,145 @@ trait SendServerRequests { import code.api.util.APIUtil.OAuth.{Consumer, Token} - implicit def Request2RequestSigner(r: Req): RequestSigner = new RequestSigner(r) + implicit def requestToRequestSigner(r: OBPReq): RequestSigner = new RequestSigner(r) - class RequestSigner(rb: Req) { - def <@(consumer: Consumer, token: Token): Req = + class RequestSigner(rb: OBPReq) { + def <@(consumer: Consumer, token: Token): OBPReq = rb <:< Map("Authorization" -> s"""DirectLogin token="${token.value}"""") - def <@(consumerAndToken: Option[(Consumer, Token)]): Req = + def <@(consumerAndToken: Option[(Consumer, Token)]): OBPReq = consumerAndToken match { case Some((_, token)) => rb <:< Map("Authorization" -> s"""DirectLogin token="${token.value}"""") case None => rb } } - case class ReqData ( - url: String, - method: String, - body: String, - body_encoding: String, - headers: Map[String, String], - query_params: Map[String,String], - form_params: Map[String,String] - ) + protected def url(s: String): OBPReq = OBPReq.url(s) + protected def host(h: String, p: Int): OBPReq = OBPReq.host(h, p) + protected def host(h: String): OBPReq = OBPReq.host(h) - def encode_% (s: String) = java.net.URLEncoder.encode(s, StandardCharsets.UTF_8.name()) + case class ReqData( + url: String, + method: String, + body: String, + body_encoding: String, + headers: List[(String, String)], + query_params: List[(String, String)] + ) - def decode_% (s: String) = java.net.URLDecoder.decode(s, StandardCharsets.UTF_8.name()) + def encode_%(s: String) = java.net.URLEncoder.encode(s, StandardCharsets.UTF_8.name()) + def decode_%(s: String) = URLDecoder.decode(s, StandardCharsets.UTF_8.name()) - def createRequest( reqData: ReqData ): Req = { - val charset = if(reqData.body_encoding == "") Charset.defaultCharset() else Charset.forName(reqData.body_encoding) - val rb = url(reqData.url) + def createRequest(reqData: ReqData): OBPReq = { + val charset = if (reqData.body_encoding == "") Charset.defaultCharset() else Charset.forName(reqData.body_encoding) + val rb = OBPReq.url(reqData.url) .setMethod(reqData.method) .setBodyEncoding(charset) .setBody(reqData.body) <:< reqData.headers - if (reqData.query_params.nonEmpty) - rb < qp.getName -> URLDecoder.decode(qp.getValue,"UTF-8")).toMap[String,String] - val form_params: Map[String,String] = r.getFormParams.asScala.map( fp => fp.getName -> fp.getValue).toMap[String,String] - val headers:Map[String,String] = r.getHeaders.entries().asScala.map (h => h.getKey -> h.getValue).toMap[String,String] - val url:String = r.getUrl - val method:String = r.getMethod - - ReqData(url, method, body, encoding, headers ++ extra_headers, query_params, form_params) + def extractParamsAndHeaders(req: OBPReq, body: String, encoding: String, extraHeaders: Map[String, String] = Map.empty): ReqData = { + ReqData( + url = req.baseUrl, + method = req.method, + body = body, + body_encoding = encoding, + headers = req.reqHeaders ++ extraHeaders, + query_params = req.queryParams + ) } + private def executeRequest(req: OBPReq): APIResponse = { + val (responseCode, bodyStr, okHeaders) = req.executeRaw() - private def ApiResponseCommonPart(req: Req) = { - for (response <- Http.default(req > as.Response(p => p))) - yield { - //{} -->parse(body) => JObject(List()) , this is not "NO Content", change "" --> JNothing - val body = if (response.getResponseBody().isEmpty) "" else response.getResponseBody() - - // Check that every response has a correlationId at Response Header - val list = response.getHeaders(ResponseHeader.`Correlation-Id`).asScala.toList - list match { - case Nil => - // Improve diagnostic information: include HTTP status, all response headers and a snippet of the body. - val status = response.getStatusCode - val headersStr = try { - // response.getHeaders().entries() returns a Java collection of header entries - response.getHeaders().entries().asScala.map(h => s"${h.getKey}: ${h.getValue}").mkString(", ") - } catch { - case _: Throwable => "unable to read headers" - } - val bodySnippet = if (body == null) { - "" - } else { - val maxLen = 1000 - if (body.length > maxLen) body.take(maxLen) + "..." else body - } - throw new Exception( - s"""There is no ${ResponseHeader.`Correlation-Id`} in response header. - |Couldn't parse response from ${req.url} - |status=$status - |headers=[$headersStr] - |body-snippet=${bodySnippet}""".stripMargin - ) - case _ => - } + if (okHeaders.values(ResponseHeader.`Correlation-Id`).asScala.isEmpty) { + throw missingCorrelationIdException(req, responseCode, bodyStr, okHeaders) + } - // Handle YAML responses: don't try to parse as JSON. Wrap YAML as a JString so tests - // that expect a JValue can still receive the body. - val contentTypeList = response.getHeaders("Content-Type").asScala.toList.map(_.toLowerCase) - val isYaml = contentTypeList.exists(_.contains("yaml")) - if (isYaml) { - APIResponse(response.getStatusCode, JString(body), Some(response.getHeaders())) - } else { - // json4s-native 3.6.x rejects primitive root values (booleans, strings, numbers, null). - // Wrap in a single-element array so the native parser accepts it, then extract the - // first element — handles all JSON primitive types generically. - val parsedBody: Option[JValue] = tryo { parse(body) }.toOption orElse - tryo { - parse(s"[$body]") match { - case JArray(v :: _) => v - case _ => throw new RuntimeException("empty array") - } - }.toOption - parsedBody match { - case Some(b) => APIResponse(response.getStatusCode, b, Some(response.getHeaders())) - case None => throw new Exception(s"couldn't parse response from ${req.url} : $body") - } - } + val contentTypeList = okHeaders.values(OBPReq.ContentTypeHeader).asScala.toList.map(_.toLowerCase) + if (contentTypeList.exists(_.contains("yaml"))) { + APIResponse(responseCode, JString(bodyStr), Some(okHeaders)) + } else { + parseJsonBody(bodyStr) match { + case Some(b) => APIResponse(responseCode, b, Some(okHeaders)) + case None => throw new Exception(s"couldn't parse response from ${req.url} : $bodyStr") } - } - - private def getAPIResponse(req : Req) : APIResponse = { - try { - Await.result(ApiResponseCommonPart(req), Duration.Inf) - } catch { - case e: Exception if e.getMessage != null && e.getMessage.contains("invalid version format") => - // Connection pool pollution detected - retry once with a fresh connection - // This happens when concurrent tests share the same HTTP client and one test's - // error response corrupts the connection state - Thread.sleep(100) // Brief delay to let connection close - Await.result(ApiResponseCommonPart(req), Duration.Inf) } } - private def getAPIResponseAsync(req: Req): Future[APIResponse] = { - ApiResponseCommonPart(req) - } + private def missingCorrelationIdException(req: OBPReq, responseCode: Int, bodyStr: String, okHeaders: OkHeaders): Exception = { + val headersStr = okHeaders.toMultimap.asScala + .flatMap { case (k, vs) => vs.asScala.map(v => s"$k: $v") } + .mkString(", ") + val maxLen = 1000 + val bodySnippet = + if (bodyStr == null) "" + else if (bodyStr.length > maxLen) bodyStr.take(maxLen) + "..." else bodyStr + new Exception( + s"""There is no ${ResponseHeader.`Correlation-Id`} in response header. + |Couldn't parse response from ${req.url} + |status=$responseCode + |headers=[$headersStr] + |body-snippet=${bodySnippet}""".stripMargin + ) + } + + private def parseJsonBody(bodyStr: String): Option[JValue] = + if (bodyStr.isEmpty) Some(JNothing) + else tryo { parse(bodyStr) }.toOption orElse + tryo { + parse(s"[$bodyStr]") match { + case JArray(v :: _) => v + case _ => throw new RuntimeException("empty array") + } + }.toOption - /** - *this method does a POST request given a URL, a JSON - */ - def makePostRequest(req: Req, json: String, headers: List[(String, String)] = Nil): APIResponse = { - val extra_headers = Map( "Content-Type" -> "application/json", - "Accept" -> "application/json") ++ headers - val reqData = extractParamsAndHeaders(req.POST, json, "UTF-8", extra_headers) - val jsonReq = createRequest(reqData) - getAPIResponse(jsonReq) - } - /** - *this method does a POST request given a URL, a JSON - */ - def makePostRequestAsync(req: Req, json: String = ""): Future[APIResponse] = { - val extra_headers = Map( "Content-Type" -> "application/json", - "Accept" -> "application/json") - val reqData = extractParamsAndHeaders(req.POST, json, "UTF-8", extra_headers) - val jsonReq = createRequest(reqData) - getAPIResponseAsync(jsonReq) - } + private def getAPIResponse(req: OBPReq): APIResponse = executeRequest(req) -// Accepts an additional option header Map - def makePostRequestAdditionalHeader(req: Req, json: String = "", params: List[(String, String)] = Nil): APIResponse = { - val extra_headers = Map( "Content-Type" -> "application/json", - "Accept" -> "application/json") ++ params - val reqData = extractParamsAndHeaders(req.POST, json, "UTF-8", extra_headers) - val jsonReq = createRequest(reqData) - getAPIResponse(jsonReq) - } + private def getAPIResponseAsync(req: OBPReq): Future[APIResponse] = + Future { scala.concurrent.blocking { getAPIResponse(req) } }(ExecutionContext.global) - def makePutRequest(req: Req, json: String, headers: (String, String) *) : APIResponse = { - val extra_headers = Map("Content-Type" -> "application/json") ++ headers.toMap - val reqData = extractParamsAndHeaders(req.PUT, json, "UTF-8", extra_headers) - val jsonReq = createRequest(reqData) - getAPIResponse(jsonReq) - } + private def sendSync(req: OBPReq, body: String = "", extraHeaders: Map[String, String] = Map.empty): APIResponse = + getAPIResponse(createRequest(extractParamsAndHeaders(req, body, "UTF-8", extraHeaders))) - def makePatchRequest(req: Req, json: String, headers: (String, String) *) : APIResponse = { - val extra_headers = Map("Content-Type" -> "application/json") ++ headers.toMap - val reqData = extractParamsAndHeaders(req.PATCH, json, "UTF-8", extra_headers) - val jsonReq = createRequest(reqData) - getAPIResponse(jsonReq) - } + private def sendAsync(req: OBPReq, body: String = "", extraHeaders: Map[String, String] = Map.empty): Future[APIResponse] = + getAPIResponseAsync(createRequest(extractParamsAndHeaders(req, body, "UTF-8", extraHeaders))) - def makePutRequestAsync(req: Req, json: String = ""): Future[APIResponse] = { - val extra_headers = Map("Content-Type" -> "application/json") - val reqData = extractParamsAndHeaders(req.PUT, json, "UTF-8", extra_headers) - val jsonReq = createRequest(reqData) - getAPIResponseAsync(jsonReq) - } + private val ContentType = OBPReq.ContentTypeHeader + private val ApplicationJson = "application/json" - /** - * this method does a GET request given a URL - */ - def makeGetRequest(req: Req, params: List[(String, String)] = Nil) : APIResponse = { - val extra_headers = Map.empty ++ params - val reqData = extractParamsAndHeaders(req.GET, "", "UTF-8", extra_headers) - val jsonReq = createRequest(reqData) - getAPIResponse(jsonReq) - } - - /** - * this method does a HEAD request given a URL - */ - def makeHeadRequest(req: Req, params: List[(String, String)] = Nil) : APIResponse = { - val extra_headers = Map.empty ++ params - val reqData = extractParamsAndHeaders(req.HEAD, "", "UTF-8", extra_headers) - val jsonReq = createRequest(reqData) - getAPIResponse(jsonReq) - } - /** - * this method does a GET request given a URL - */ - def makeGetRequestAsync(req: Req, params: List[(String, String)] = Nil): Future[APIResponse] = { - val extra_headers = Map.empty ++ params - val reqData = extractParamsAndHeaders(req.GET, "", "UTF-8", extra_headers) - val jsonReq = createRequest(reqData) - getAPIResponseAsync(jsonReq) - } + private val jsonHeaders: Map[String, String] = Map(ContentType -> ApplicationJson, "Accept" -> ApplicationJson) + private val putHeaders: Map[String, String] = Map(ContentType -> ApplicationJson) - /** - * this method does a delete request given a URL - */ - def makeDeleteRequest(req: Req) : APIResponse = { - //Note: method will be set too late for oauth signing, so set it before using <@ - val jsonReq = req.DELETE - getAPIResponse(jsonReq) - } - /** - * this method does a delete request given a URL - */ - def makeDeleteRequestAsync(req: Req): Future[APIResponse] = { - //Note: method will be set too late for oauth signing, so set it before using <@ - val jsonReq = req.DELETE - getAPIResponseAsync(jsonReq) - } + def makePostRequest(req: OBPReq, json: String, headers: List[(String, String)] = Nil): APIResponse = + sendSync(req.POST, json, jsonHeaders ++ headers) + + def makePostRequestAsync(req: OBPReq, json: String = ""): Future[APIResponse] = + sendAsync(req.POST, json, jsonHeaders) + + def makePostRequestAdditionalHeader(req: OBPReq, json: String = "", params: List[(String, String)] = Nil): APIResponse = + sendSync(req.POST, json, jsonHeaders ++ params) + + def makePutRequest(req: OBPReq, json: String, headers: (String, String)*): APIResponse = + sendSync(req.PUT, json, putHeaders ++ headers.toMap) + + def makePatchRequest(req: OBPReq, json: String, headers: (String, String)*): APIResponse = + sendSync(req.PATCH, json, putHeaders ++ headers.toMap) + + def makePutRequestAsync(req: OBPReq, json: String = ""): Future[APIResponse] = + sendAsync(req.PUT, json, putHeaders) + + def makeGetRequest(req: OBPReq, params: List[(String, String)] = Nil): APIResponse = + sendSync(req.GET, extraHeaders = Map.empty ++ params) + + def makeHeadRequest(req: OBPReq, params: List[(String, String)] = Nil): APIResponse = + sendSync(req.HEAD, extraHeaders = Map.empty ++ params) + + def makeGetRequestAsync(req: OBPReq, params: List[(String, String)] = Nil): Future[APIResponse] = + sendAsync(req.GET, extraHeaders = Map.empty ++ params) + + def makeDeleteRequest(req: OBPReq): APIResponse = getAPIResponse(req.DELETE) + + def makeDeleteRequestAsync(req: OBPReq): Future[APIResponse] = getAPIResponseAsync(req.DELETE) } diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala index 199209715e..e09b0c704a 100644 --- a/obp-api/src/test/scala/code/setup/ServerSetup.scala +++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala @@ -38,7 +38,6 @@ import code.model.{Consumer, Nonce, Token} import code.model.dataAccess.{AuthUser, ResourceUser} import code.util.Helper.MdcLoggable import com.openbankproject.commons.model.{AccountId, BankId} -import dispatch._ import net.liftweb.common.{Empty, Full} import org.json4s.JsonDSL._ import net.liftweb.mapper.MetaMapper diff --git a/obp-api/src/test/scala/code/setup/ServerSetupAsync.scala b/obp-api/src/test/scala/code/setup/ServerSetupAsync.scala deleted file mode 100644 index 320a9c797d..0000000000 --- a/obp-api/src/test/scala/code/setup/ServerSetupAsync.scala +++ /dev/null @@ -1,110 +0,0 @@ -/** -Open Bank Project - API -Copyright (C) 2011-2019, TESOBE GmbH. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -Email: contact@tesobe.com -TESOBE GmbH. -Osloer Strasse 16/17 -Berlin 13359, Germany - -This product includes software developed at -TESOBE (http://www.tesobe.com/) - - */ - -package code.setup - -import org.json4s._ -import java.text.SimpleDateFormat - -import _root_.org.json4s.JsonAST.JObject -import code.TestServer -import code.api.util.{APIUtil, CustomJsonFormats} -import code.util.Helper.MdcLoggable -import com.openbankproject.commons.model.{AccountId, BankId} -import dispatch._ -import net.liftweb.common.{Empty, Full} -import org.json4s.JsonDSL._ -import org.scalatest._ - -trait ServerSetupAsync extends AsyncFeatureSpec with SendServerRequests - with BeforeAndAfterEach with GivenWhenThen - with BeforeAndAfterAll - with Matchers with MdcLoggable { - - implicit val formats = CustomJsonFormats.emptyHintFormats - - val server = TestServer - def baseRequest = host(server.host, server.port) - val secured = APIUtil.getPropsAsBoolValue("external.https", false) - def externalBaseRequest = (server.externalHost, server.externalPort) match { - case (Full(h), Full(p)) if secured => host(h, p).secure - case (Full(h), Full(p)) if !secured => host(h, p) - case (Full(h), Empty) if secured => host(h).secure - case (Full(h), Empty) if !secured => host(h) - case (Full(h), Empty) => host(h) - case _ => baseRequest - } - - // @code.setup.TestConnectorSetup.createBanks we can know, the bankIds in test database. - val testBankId1 = BankId("testBank1") - val testBankId2 = BankId("testBank2") - - // @code.setup.TestConnectorSetup.createAccounts we can know, the accountIds in test database. - val testAccountId1 = AccountId("testAccount1") - val testAccountId2 = AccountId("testAccount2") - - val mockCustomerNumber1 = "93934903201" - val mockCustomerNumber2 = "93934903202" - - val mockCustomerNumber = "93934903208565488" - val mockCustomerId = "cba6c9ef-73fa-4032-9546-c6f6496b354a" - - val emptyJSON : JObject = ("error" -> "empty List") - val errorAPIResponse = new APIResponse(400,emptyJSON, None) - -} - -trait ServerSetupWithTestDataAsync extends ServerSetupAsync with DefaultConnectorTestSetup with DefaultUsers { - - // On-demand test data (mirrors ServerSetupWithTestData). No async suite reads the - // beforeEach-created transactions / transaction-requests, so the whitelist is empty and both - // are skipped for every async suite. Add a simple class name here (or override - // needsTransactionData) if a future async suite ever needs them. - protected val suitesNeedingTransactionData: Set[String] = Set.empty - protected def needsTransactionData: Boolean = - suitesNeedingTransactionData.contains(this.getClass.getSimpleName) - - override def beforeEach() = { - super.beforeEach() - //create fake data for the tests - //fake banks - val banks = createBanks() - //fake bank accounts - val accounts = createAccountRelevantResources(resourceUser1, banks) - //fake transactions + transactionRequests — opt-in per suite (none currently) - if (needsTransactionData) { - createTransactions(accounts) - createTransactionRequests(accounts) - } - } - - override def afterEach() = { - super.afterEach() - wipeTestData() - } - -} \ No newline at end of file diff --git a/obp-api/src/test/scala/code/util/DynamicUtilTest.scala b/obp-api/src/test/scala/code/util/DynamicUtilTest.scala index abd2d9e748..c8123087eb 100644 --- a/obp-api/src/test/scala/code/util/DynamicUtilTest.scala +++ b/obp-api/src/test/scala/code/util/DynamicUtilTest.scala @@ -45,6 +45,9 @@ import scala.io.Source class DynamicUtilTest extends FlatSpec with Matchers { object DynamicUtilsTag extends Tag("DynamicUtil") + private val securityManagerUnavailable = + "SecurityManager enforcement is not available on JDK 17+ (JEP 411); skip on JDK 21" + implicit val formats = code.api.util.CustomJsonFormats.formats @@ -122,6 +125,7 @@ class DynamicUtilTest extends FlatSpec with Matchers { } "Sandbox.createSandbox method" should "should throw exception" taggedAs DynamicUtilsTag in { + assume(System.getSecurityManager != null, securityManagerUnavailable) val permissionList = List( // new java.net.SocketPermission("ir.dcs.gla.ac.uk:80","connect,resolve"), ) @@ -146,6 +150,7 @@ class DynamicUtilTest extends FlatSpec with Matchers { } "Sandbox.sandbox method test bankId" should "should throw exception" taggedAs DynamicUtilsTag in { + assume(System.getSecurityManager != null, securityManagerUnavailable) intercept[AccessControlException] { Sandbox.sandbox(bankId= "abc").runInSandbox { BankId("123" ) @@ -160,6 +165,7 @@ class DynamicUtilTest extends FlatSpec with Matchers { } "Sandbox.sandbox method test default permission" should "should throw exception" taggedAs DynamicUtilsTag in { + assume(System.getSecurityManager != null, securityManagerUnavailable) intercept[AccessControlException] { Sandbox.sandbox(bankId= "abc").runInSandbox { scala.io.Source.fromURL("https://apisandbox.openbankproject.com/") diff --git a/pom.xml b/pom.xml index 6ecae876c8..6bbe6215fd 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ 2011 2.12 - 2.12.20 + 2.12.21 1.1.5 1.1.0 4.1.2 @@ -27,10 +27,14 @@ scaladocs/ http://scala-tools.org/mvnsites/liftweb - - 11 + + 25 ${java.version} ${java.version} + + 11 @@ -160,6 +164,7 @@ 3.0.8 test + diff --git a/run_tests_parallel.sh b/run_tests_parallel.sh index b35461cebd..2c9b57eb2d 100755 --- a/run_tests_parallel.sh +++ b/run_tests_parallel.sh @@ -1,8 +1,17 @@ #!/bin/bash -# Local parallel test runner — mirror CI's parallel structure as closely as -# possible while dropping the cross-machine artifact-transfer complexity. -# Shard definitions and the shard-4 catch-all logic match -# .github/workflows/build_pull_request.yml exactly. +# Local parallel test runner — mirrors CI's test coverage on a single machine. +# Pinned to JDK 25 (Scala 2.12.21+). Override JAVA_HOME before running if a +# different JDK is needed. +JAVA25_HOME="/Library/Java/JavaVirtualMachines/zulu-25.jdk/Contents/Home" +if [[ -d "$JAVA25_HOME" ]]; then + export JAVA_HOME="$JAVA25_HOME" + export PATH="$JAVA_HOME/bin:$PATH" +fi +# CI (build_pull_request.yml / build_container.yml) uses 9 shards across 9 VMs; +# this script uses 4 coarser shards that achieve identical coverage via the +# catch-all mechanism, without exhausting the single local DB connection pool +# (> 4 shards causes connection-pool contention and spurious failures). +# Catch-all logic (build_s4) is a direct port of CI's shard-8 catch-all. # Usage: ./run_tests_parallel.sh [--shards=4|6] # # ── CI step → local equivalent (how cross-machine machinery is replaced) ─── @@ -84,7 +93,7 @@ ALLOC_PORT="" # alloc_free_port returns its result here (no subshell — s # breaking the in-run dedup. Call as: `alloc_free_port || exit 1; X=$ALLOC_PORT`. alloc_free_port() { local tries=0 p - while [ $tries -lt 500 ]; do + while [[ $tries -lt 500 ]]; do p=$(( PORT_MIN + RANDOM % (PORT_MAX - PORT_MIN) )) if [[ " ${ASSIGNED_PORTS[*]} " != *" $p "* ]] && ! lsof -i :"$p" >/dev/null 2>&1; then ASSIGNED_PORTS+=("$p") @@ -97,7 +106,11 @@ alloc_free_port() { return 1 } -# ── Shard definitions (identical to the CI matrix) ──────────────────────── +# ── Shard definitions ───────────────────────────────────────────────────── +# Deliberately coarser than CI's 9 shards: CI splits each package onto its own +# VM; locally we merge packages to stay within the shared DB connection pool. +# Coverage is identical: the catch-all (build_s4) picks up any package not +# named here, same as CI's shard-8 catch-all. S1="code.api.v4_0_0" S2="code.api.v6_0_0,code.api.v5_0_0,code.api.v3_0_0,code.api.v2_1_0,\ @@ -109,13 +122,13 @@ S3="code.api.v1_2_1,code.api.ResourceDocs1_4_0,code.api.util,code.api.berlin,\ code.management,code.metrics,code.model,code.views,code.usercustomerlinks,\ code.customer,code.errormessages" -# Shard 4 base (identical to CI) +# Shard 4 base — auth/login/connector/util plus any packages not in shards 1-3 S4_BASE="code.api.v5_1_0,code.api.v3_1_0,code.api.http4sbridge,code.api.v7_0_0,\ code.api.Authentication,code.api.dauthTest,code.api.DirectLoginTest,\ code.api.gateWayloginTest,code.api.OBPRestHelperTest,code.util,code.connector" # ── Shard 4 catch-all: discover every package not covered by shards 1–3 ─── -# (identical to CI) +# (same logic as CI shard-8 catch-all — ensures no new package is silently skipped) build_s4() { local ASSIGNED="$S1 $(echo "$S2" | tr ',' ' ') $(echo "$S3" | tr ',' ' ') $(echo "$S4_BASE" | tr ',' ' ')" local ALL_PKGS @@ -131,9 +144,9 @@ build_s4() { covered=true; break fi done - [ "$covered" = "false" ] && EXTRAS="${EXTRAS},${pkg}" + [[ "$covered" = "false" ]] && EXTRAS="${EXTRAS},${pkg}" done - if [ -n "$EXTRAS" ]; then + if [[ -n "$EXTRAS" ]]; then echo " [Shard 4] Catch-all extras: $EXTRAS" >&2 fi echo "${S4_BASE}${EXTRAS}" @@ -190,14 +203,20 @@ run_shard() { # timeout returns 124 when the JVM was killed. That is only benign when the tests had # already finished green and only the JVM shutdown hung (Pekko non-daemon threads) — # require proof from the log instead of blindly converting 124 to success. - if [ $rc -eq 124 ]; then + if [[ $rc -eq 124 ]]; then if grep -q "BUILD SUCCESS" "$log" 2>/dev/null; then rc=0 else echo "[Shard $n] ⏱ timeout: JVM killed BEFORE tests completed — counted as failure" fi fi - if [ $rc -eq 0 ]; then + # maven.test.failure.ignore=true (root pom) makes mvn exit 0 even when suites + # abort or tests fail — the exit code alone is not trustworthy. Scan the log for + # scalatest's own failure markers (RUN ABORTED / SUITE ABORTED / failed N). + if [[ $rc -eq 0 ]] && grep -qE '\*\*\* RUN ABORTED \*\*\*|SUITE(S)? ABORTED|Tests: succeeded [0-9]+, failed [1-9]' "$log"; then + rc=1 + fi + if [[ $rc -eq 0 ]]; then echo "[Shard $n] ✅ BUILD SUCCESS" else echo "[Shard $n] ❌ BUILD FAILURE — see $log" @@ -233,13 +252,13 @@ MAVEN_OPTS="$MVN_OPTS" \ mvn install -DskipTests -pl obp-commons -q > test-results/parallel/precompile.log 2>&1 PRECOMPILE_RC=$? rm -rf "$OBC_LOCK" -if [ $PRECOMPILE_RC -eq 0 ]; then +if [[ $PRECOMPILE_RC -eq 0 ]]; then echo "Pre-compile 2/2: test-compile obp-api -> shared target/ ..." MAVEN_OPTS="$MVN_OPTS" \ mvn test-compile -pl obp-api -q >> test-results/parallel/precompile.log 2>&1 PRECOMPILE_RC=$? fi -if [ $PRECOMPILE_RC -ne 0 ]; then +if [[ $PRECOMPILE_RC -ne 0 ]]; then echo "❌ Pre-compile failed — see test-results/parallel/precompile.log" >&2 tail -25 test-results/parallel/precompile.log >&2 exit 1 @@ -251,7 +270,7 @@ rm -rf obp-api/target/surefire-reports obp-commons/target/surefire-reports echo "Pre-compile done, starting shards..." echo "" -if [ "$SHARDS" = "6" ]; then +if [[ "$SHARDS" = "6" ]]; then echo "Starting 6 shards in parallel..." echo "" # Allocate two free ports per shard BEFORE forking. Sequential calls (not in a @@ -320,15 +339,15 @@ done OVERALL_RC=0 for rc in "${RCS[@]}"; do - [ $rc -ne 0 ] && OVERALL_RC=1 + [[ $rc -ne 0 ]] && OVERALL_RC=1 done # ── CI parity ("Report failing tests" step): extract failures for failed shards ── -if [ $OVERALL_RC -ne 0 ]; then +if [[ $OVERALL_RC -ne 0 ]]; then echo "" echo "── Failure diagnostics (CI-style report) ───────────" for (( n=1; n<=TOTAL_SHARDS; n++ )); do - [ "${RCS[$((n-1))]}" -eq 0 ] && continue + [[ "${RCS[$((n-1))]}" -eq 0 ]] && continue log="test-results/parallel/shard${n}.log" echo "" echo "### Shard $n ($log) ###" @@ -367,14 +386,14 @@ for b in bad: read -r SF_TOTAL SF_FAIL SF_ERR SF_SKIP SF_BROKEN <<< "$(echo "$SF_AUDIT" | head -1)" echo "" echo "Surefire audit: ${SF_TOTAL:-?} tests, ${SF_FAIL:-?} failures, ${SF_ERR:-?} errors, ${SF_SKIP:-?} skipped/canceled" -if [ "${SF_FAIL:-1}" != "0" ] || [ "${SF_ERR:-1}" != "0" ] || [ "${SF_BROKEN:-1}" != "0" ]; then +if [[ "${SF_FAIL:-1}" != "0" ]] || [[ "${SF_ERR:-1}" != "0" ]] || [[ "${SF_BROKEN:-1}" != "0" ]]; then echo "$SF_AUDIT" | tail -n +2 | sed 's/^/ ✗ /' OVERALL_RC=1 fi # Zero-test floor: -DfailIfNoTests=false means a broken wildcardSuites filter runs nothing # and "passes". The suite has ~2900 tests; a total far below that means shards ran # near-empty — fail instead of reporting a hollow green. -if [ "${SF_TOTAL:-0}" -lt 2000 ]; then +if [[ "${SF_TOTAL:-0}" -lt 2000 ]]; then echo " ✗ suspicious total: only ${SF_TOTAL:-0} tests ran (< 2000 floor) — filter/discovery regression?" OVERALL_RC=1 fi @@ -392,7 +411,7 @@ fi # Final verdict LAST so `tail -N` always captures it, plus a machine-readable file # that survives any piping of stdout (`./run.sh | tail` reports tail's exit code). echo "" -if [ $OVERALL_RC -eq 0 ]; then +if [[ $OVERALL_RC -eq 0 ]]; then echo "✅ ALL SHARDS PASSED" echo "PASS" > test-results/parallel/RESULT else diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000000..3185093de2 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1 @@ +sonar.cpd.exclusions=obp-api/src/test/**/*.scala,obp-api/src/test/**/*.java diff --git a/sonarcloud-login.png b/sonarcloud-login.png deleted file mode 100644 index e08d411234..0000000000 Binary files a/sonarcloud-login.png and /dev/null differ diff --git a/sonarcloud-overview.png b/sonarcloud-overview.png deleted file mode 100644 index 67b5eea6e7..0000000000 Binary files a/sonarcloud-overview.png and /dev/null differ diff --git a/test_graalvm_quick.sh b/test_graalvm_quick.sh deleted file mode 100755 index a108b8970b..0000000000 --- a/test_graalvm_quick.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -# Quick GraalVM test - Run this in your terminal with Java 11 - -echo "Java version:" -java -version -echo "" - -echo "Running DynamicMessageDocTest (this should pass with Java 11)..." -MAVEN_OPTS="-Xms3G -Xmx6G -XX:MaxMetaspaceSize=2G -XX:+UseG1GC" \ - mvn scalatest:test -Dsuites=code.api.v4_0_0.DynamicMessageDocTest -pl obp-api -T 4 -o - -echo "" -echo "Check if test passed above. If you see 'BUILD SUCCESS' and no NoSuchMethodError, the fix works!" diff --git a/zed/.metals-config.json b/zed/.metals-config.json deleted file mode 100644 index ed54fa6477..0000000000 --- a/zed/.metals-config.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "maven": { - "enabled": true - }, - "metals": { - "serverVersion": "1.0.0", - "javaHome": "/usr/lib/jvm/java-17-openjdk-amd64", - "bloopVersion": "2.0.0", - "superMethodLensesEnabled": true, - "enableSemanticHighlighting": true, - "compileOnSave": true, - "testUserInterface": "Code Lenses", - "inlayHints": { - "enabled": true, - "hintsInPatternMatch": { - "enabled": true - }, - "implicitArguments": { - "enabled": true - }, - "implicitConversions": { - "enabled": true - }, - "inferredTypes": { - "enabled": true - }, - "typeParameters": { - "enabled": true - } - } - }, - "buildTargets": [ - { - "id": "obp-commons", - "displayName": "obp-commons", - "baseDirectory": "file:///home/marko/Tesobe/GitHub/constantine2nd/OBP-API/obp-commons/", - "tags": ["library"], - "languageIds": ["scala", "java"], - "dependencies": [], - "capabilities": { - "canCompile": true, - "canTest": true, - "canRun": false, - "canDebug": true - }, - "dataKind": "scala", - "data": { - "scalaOrganization": "org.scala-lang", - "scalaVersion": "2.12.20", - "scalaBinaryVersion": "2.12", - "platform": "jvm" - } - }, - { - "id": "obp-api", - "displayName": "obp-api", - "baseDirectory": "file:///home/marko/Tesobe/GitHub/constantine2nd/OBP-API/obp-api/", - "tags": ["application"], - "languageIds": ["scala", "java"], - "dependencies": ["obp-commons"], - "capabilities": { - "canCompile": true, - "canTest": true, - "canRun": true, - "canDebug": true - }, - "dataKind": "scala", - "data": { - "scalaOrganization": "org.scala-lang", - "scalaVersion": "2.12.20", - "scalaBinaryVersion": "2.12", - "platform": "jvm" - } - } - ] -} diff --git a/zed/README.md b/zed/README.md deleted file mode 100644 index 88ecd432a6..0000000000 --- a/zed/README.md +++ /dev/null @@ -1,298 +0,0 @@ -# ZED IDE Setup for OBP-API Development - -> **Complete ZED IDE integration for the Open Bank Project API** - -This folder contains everything needed to set up ZED IDE with full Scala language server support, automated build tasks, and streamlined development workflows for OBP-API. - -## 🚀 Quick Setup (5 minutes) - -### Prerequisites - -- **Java 17+** (OpenJDK recommended) -- **Maven 3.6+** -- **ZED IDE** (latest version) - -### Single Setup Script - -```bash -cd OBP-API -./zed/setup-zed-ide.sh -``` - -This unified script automatically: - -- ✅ Installs missing dependencies (Coursier, Bloop) -- ✅ Compiles the project and resolves dependencies -- ✅ Generates dynamic Bloop configurations -- ✅ Sets up Metals language server -- ✅ Copies ZED configuration files to `.zed/` folder -- ✅ Configures build and run tasks -- ✅ Sets up manual-only code formatting - -## 📁 What's Included - -``` -zed/ -├── README.md # This comprehensive guide -├── setup-zed-ide.sh # Single unified setup script -├── generate-bloop-config.sh # Dynamic Bloop config generator -├── settings.json # ZED IDE settings template -├── tasks.json # Pre-configured build/run tasks -├── .metals-config.json # Metals language server config -└── setup-zed.bat # Windows setup script -``` - -## ⌨️ Essential Keyboard Shortcuts - -| Action | Linux | macOS/Windows | Purpose | -| -------------------- | -------------- | ------------- | ----------------------------- | -| **Command Palette** | `Ctrl+Shift+P` | `Cmd+Shift+P` | Access all tasks | -| **Go to Definition** | `F12` | `F12` | Navigate to symbol definition | -| **Find References** | `Shift+F12` | `Shift+F12` | Find all symbol usages | -| **Quick Open File** | `Ctrl+P` | `Cmd+P` | Fast file navigation | -| **Format Code** | `Ctrl+Shift+I` | `Cmd+Shift+I` | Auto-format Scala code | -| **Symbol Search** | `Ctrl+T` | `Cmd+T` | Search symbols project-wide | - -## 🛠️ Available Development Tasks - -Access via Command Palette (`Ctrl+Shift+P` on Linux, `Cmd+Shift+P` on macOS/Windows) → `"task: spawn"` (Linux) or `"Tasks: Spawn"` (macOS/Windows): - -### Core Development Tasks - -| Task | Purpose | Duration | When to Use | -| ---------------------------- | ------------------------ | --------- | ------------------------------------ | -| **Quick Build Dependencies** | Build only dependencies | 1-3 min | First step, after dependency changes | -| **[1] Run OBP-API Server** | Start development server | 3-5 min | Daily development | -| **🔨 Build OBP-API** | Full project build | 2-5 min | After code changes | -| **Run Tests** | Execute test suite | 5-15 min | Before commits | -| **[3] Compile Only** | Quick syntax check | 30s-1 min | During development | - -### Utility Tasks - -| Task | Purpose | -| --------------------------------- | ------------------------- | -| **[4] Clean Target Folders** | Remove build artifacts | -| **🔄 Continuous Compile (Scala)** | Auto-recompile on changes | -| **[2] Test API Root Endpoint** | Verify server status | -| **🔧 Kill Server on Port 8080** | Stop stuck processes | -| **🔍 Check Dependencies** | Verify Maven dependencies | - -## 🏗️ Development Workflow - -### Daily Development - -1. **Start Development Session** - - Linux: `Ctrl+Shift+P` → `"task: spawn"` → `"Quick Build Dependencies"` - - macOS: `Cmd+Shift+P` → `"Tasks: Spawn"` → `"Quick Build Dependencies"` - -2. **Start API Server** - - Use task `"[1] Run OBP-API Server"` - - Server runs on: `http://localhost:8080` - - Test endpoint: `http://localhost:8080/obp/v5.1.0/root` - -3. **Code Development** - - Edit Scala files in `obp-api/src/main/scala/` - - Use `F12` for Go to Definition - - Auto-completion with `Ctrl+Space` - - Real-time error highlighting - - Format code with `Ctrl+Shift+I` - -4. **Testing & Validation** - - Quick compile: `"[3] Compile Only"` task - - Run tests: `"Run Tests"` task - - API testing: `"[2] Test API Root Endpoint"` task - -## 🔧 Configuration Details - -### ZED IDE Settings (`settings.json`) - -- **Format on Save**: DISABLED (manual formatting only - use `Ctrl+Shift+I`) -- **Scala LSP**: Optimized Metals configuration -- **Maven Integration**: Proper MAVEN_OPTS for Java 17+ -- **UI Preferences**: One Dark theme, consistent layout -- **Inlay Hints**: Enabled for better code understanding - -### Build Tasks (`tasks.json`) - -All tasks include proper environment variables: - -```bash -MAVEN_OPTS="-Xss128m --add-opens=java.base/java.util.jar=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" -``` - -### Metals LSP (`.metals-config.json`) - -- **Build Tool**: Maven -- **Bloop Integration**: Dynamic configuration generation -- **Scala Version**: 2.12.20 -- **Java Target**: Java 11 (compatible with Java 17) - -## 🚨 Troubleshooting - -### Common Issues - -| Problem | Symptom | Solution | -| ------------------------------- | ------------------------------------ | ------------------------------------------------ | -| **Language Server Not Working** | No go-to-definition, no autocomplete | Restart ZED, wait for Metals initialization | -| **Compilation Errors** | Red squiggly lines, build failures | Check Problems panel, run "Clean Target Folders" | -| **Server Won't Start** | Port 8080 busy | Run "Kill Server on Port 8080" task | -| **Out of Memory** | Build fails with heap space error | Already configured in tasks | -| **Missing Dependencies** | Import errors | Run "Check Dependencies" task | - -### Recovery Procedures - -1. **Full Reset**: - - ```bash - ./zed/setup-zed-ide.sh # Re-run complete setup - ``` - -2. **Regenerate Bloop Configurations**: - - ```bash - ./zed/generate-bloop-config.sh # Regenerate configs - ``` - -3. **Clean Restart**: - - Clean build with "Clean Target Folders" task - - Restart ZED IDE - - Wait for Metals to reinitialize (2-3 minutes) - -### Platform-Specific Notes - -#### Linux Users - -- Use `"task: spawn"` in command palette (not `"Tasks: Spawn"`) -- Ensure proper Java permissions for Maven - -#### macOS/Windows Users - -- Use `"Tasks: Spawn"` in command palette -- Windows users can also use `setup-zed.bat` - -## 🌐 API Development - -### Project Structure - -``` -OBP-API/ -├── obp-api/ # Main API application -│ └── src/main/scala/ # Scala source code -│ └── code/api/ # API endpoint definitions -│ ├── v5_1_0/ # Latest API version -│ ├── v4_0_0/ # Previous versions -│ └── util/ # Utility functions -├── obp-commons/ # Shared utilities and models -│ └── src/main/scala/ # Common Scala code -└── .zed/ # ZED IDE configuration (generated) -``` - -### Adding New API Endpoints - -1. Navigate to `obp-api/src/main/scala/code/api/v5_1_0/` -2. Find appropriate API trait (e.g., `OBPAPI5_1_0.scala`) -3. Follow existing endpoint patterns -4. Use `F12` to navigate to helper functions -5. Test with API test task - -### Testing Endpoints - -```bash -# Root API information -curl http://localhost:8080/obp/v5.1.0/root - -# Health check -curl http://localhost:8080/obp/v5.1.0/config - -# Banks list (requires proper setup) -curl http://localhost:8080/obp/v5.1.0/banks -``` - -## 🎯 Pro Tips - -### Code Navigation - -- **Quick file access**: `Ctrl+P` then type filename -- **Symbol search**: `Ctrl+T` then type function/class name -- **Project-wide text search**: `Ctrl+Shift+F` - -### Efficiency Shortcuts - -- `Ctrl+/` - Toggle line comment -- `Ctrl+D` - Select next occurrence -- `Ctrl+Shift+L` - Select all occurrences -- `F2` - Rename symbol -- `Alt+←/→` - Navigate back/forward - -### Performance Optimization - -- Close unused files to reduce memory usage -- Use "Continuous Compile" for faster feedback -- Limit test runs to specific modules during development - -## 📚 Additional Resources - -### Documentation - -- **OBP-API Project**: https://github.com/OpenBankProject/OBP-API -- **API Documentation**: https://apiexplorer.openbankproject.com -- **Community Forums**: https://openbankproject.com - -### Learning Resources - -- **Scala**: https://docs.scala-lang.org/ -- **Lift Framework**: https://liftweb.net/ -- **Maven**: https://maven.apache.org/guides/ -- **ZED IDE**: https://zed.dev/docs - -## 🆘 Getting Help - -### Diagnostic Commands - -```bash -# Check Java version -java -version - -# Check Maven -mvn -version - -# Check Bloop status -bloop projects - -# Test compilation -bloop compile obp-commons obp-api - -# Check ZED configuration -ls -la .zed/ -``` - -### Common Error Messages - -| Error | Cause | Solution | -| --------------------------- | ----------------------------- | ----------------------------- | -| "Java module system" errors | Java 17+ module restrictions | Already handled in MAVEN_OPTS | -| "Port 8080 already in use" | Previous server still running | Use "Kill Server" task | -| "Metals not responding" | Language server crashed | Restart ZED IDE | -| "Compilation failed" | Dependency issues | Run "Check Dependencies" | - ---- - -## 🎉 Getting Started Checklist - -- [ ] Install Java 17+, Maven 3.6+, ZED IDE -- [ ] Clone OBP-API repository -- [ ] Run `./zed/setup-zed-ide.sh` (single setup script) -- [ ] Open project in ZED IDE -- [ ] Wait for Metals initialization (2-3 minutes) -- [ ] Run "Quick Build Dependencies" task -- [ ] Start server with "[1] Run OBP-API Server" task -- [ ] Test API at http://localhost:8080/obp/v5.1.0/root -- [ ] Try "Go to Definition" (F12) on Scala symbol -- [ ] Format code manually with `Ctrl+Shift+I` (auto-format disabled) -- [ ] Make a small code change and test compilation - -**Welcome to productive OBP-API development with ZED IDE! 🚀** - ---- - -_This setup provides a complete, optimized development environment for the Open Bank Project API using ZED IDE with full Scala language server support._ diff --git a/zed/generate-bloop-config.sh b/zed/generate-bloop-config.sh deleted file mode 100755 index 698e7d6aeb..0000000000 --- a/zed/generate-bloop-config.sh +++ /dev/null @@ -1,263 +0,0 @@ -#!/bin/bash - -# Generate portable Bloop configuration files for OBP-API -# This script creates Bloop JSON configurations with proper paths for any system - -set -e - -echo "🔧 Generating Bloop configuration files..." - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Get the project root directory (parent of zed folder) -PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -echo "📁 Project root: $PROJECT_ROOT" - -# Check if we're in the zed directory and project structure exists -if [[ ! -f "$PROJECT_ROOT/pom.xml" ]] || [[ ! -d "$PROJECT_ROOT/obp-api" ]] || [[ ! -d "$PROJECT_ROOT/obp-commons" ]]; then - echo -e "${RED}❌ Error: Could not find OBP-API project structure${NC}" - echo "Make sure you're running this from the zed/ folder of the OBP-API project" - exit 1 -fi - -# Change to project root for Maven operations -cd "$PROJECT_ROOT" - -# Detect Java home -if [[ -z "$JAVA_HOME" ]]; then - JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java)))) - echo -e "${YELLOW}⚠️ JAVA_HOME not set, detected: $JAVA_HOME${NC}" -else - echo -e "${GREEN}✅ JAVA_HOME: $JAVA_HOME${NC}" -fi - -# Get Maven local repository -M2_REPO=$(mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout 2>/dev/null || echo "$HOME/.m2/repository") -echo "📦 Maven repository: $M2_REPO" - -# Ensure .bloop directory exists in project root -mkdir -p "$PROJECT_ROOT/.bloop" - -# Generate obp-commons.json -echo "🔨 Generating obp-commons configuration..." -cat > "$PROJECT_ROOT/.bloop/obp-commons.json" << EOF -{ - "version": "1.5.5", - "project": { - "name": "obp-commons", - "directory": "${PROJECT_ROOT}/obp-commons", - "workspaceDir": "${PROJECT_ROOT}", - "sources": [ - "${PROJECT_ROOT}/obp-commons/src/main/scala", - "${PROJECT_ROOT}/obp-commons/src/main/java" - ], - "dependencies": [], - "classpath": [ - "${PROJECT_ROOT}/obp-commons/target/classes", - "${M2_REPO}/net/liftweb/lift-common_2.12/3.5.0/lift-common_2.12-3.5.0.jar", - "${M2_REPO}/org/scala-lang/scala-library/2.12.12/scala-library-2.12.12.jar", - "${M2_REPO}/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar", - "${M2_REPO}/org/scala-lang/modules/scala-xml_2.12/1.3.0/scala-xml_2.12-1.3.0.jar", - "${M2_REPO}/org/scala-lang/modules/scala-parser-combinators_2.12/1.1.2/scala-parser-combinators_2.12-1.1.2.jar", - "${M2_REPO}/net/liftweb/lift-util_2.12/3.5.0/lift-util_2.12-3.5.0.jar", - "${M2_REPO}/org/scala-lang/scala-compiler/2.12.12/scala-compiler-2.12.12.jar", - "${M2_REPO}/net/liftweb/lift-actor_2.12/3.5.0/lift-actor_2.12-3.5.0.jar", - "${M2_REPO}/net/liftweb/lift-markdown_2.12/3.5.0/lift-markdown_2.12-3.5.0.jar", - "${M2_REPO}/joda-time/joda-time/2.10/joda-time-2.10.jar", - "${M2_REPO}/org/joda/joda-convert/2.1/joda-convert-2.1.jar", - "${M2_REPO}/commons-codec/commons-codec/1.11/commons-codec-1.11.jar", - "${M2_REPO}/nu/validator/htmlparser/1.4.12/htmlparser-1.4.12.jar", - "${M2_REPO}/xerces/xercesImpl/2.11.0/xercesImpl-2.11.0.jar", - "${M2_REPO}/xml-apis/xml-apis/1.4.01/xml-apis-1.4.01.jar", - "${M2_REPO}/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar", - "${M2_REPO}/net/liftweb/lift-mapper_2.12/3.5.0/lift-mapper_2.12-3.5.0.jar", - "${M2_REPO}/net/liftweb/lift-db_2.12/3.5.0/lift-db_2.12-3.5.0.jar", - "${M2_REPO}/net/liftweb/lift-webkit_2.12/3.5.0/lift-webkit_2.12-3.5.0.jar", - "${M2_REPO}/commons-fileupload/commons-fileupload/1.3.3/commons-fileupload-1.3.3.jar", - "${M2_REPO}/commons-io/commons-io/2.2/commons-io-2.2.jar", - "${M2_REPO}/org/mozilla/rhino/1.7.10/rhino-1.7.10.jar", - "${M2_REPO}/net/liftweb/lift-proto_2.12/3.5.0/lift-proto_2.12-3.5.0.jar", - "${M2_REPO}/org/scala-lang/scala-reflect/2.12.20/scala-reflect-2.12.20.jar", - "${M2_REPO}/org/scalatest/scalatest_2.12/3.0.8/scalatest_2.12-3.0.8.jar", - "${M2_REPO}/org/scalactic/scalactic_2.12/3.0.8/scalactic_2.12-3.0.8.jar", - "${M2_REPO}/net/liftweb/lift-json_2.12/3.5.0/lift-json_2.12-3.5.0.jar", - "${M2_REPO}/org/scala-lang/scalap/2.12.12/scalap-2.12.12.jar", - "${M2_REPO}/com/thoughtworks/paranamer/paranamer/2.8/paranamer-2.8.jar", - "${M2_REPO}/com/alibaba/transmittable-thread-local/2.11.5/transmittable-thread-local-2.11.5.jar", - "${M2_REPO}/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar", - "${M2_REPO}/org/apache/commons/commons-text/1.10.0/commons-text-1.10.0.jar", - "${M2_REPO}/com/google/guava/guava/32.0.0-jre/guava-32.0.0-jre.jar", - "${M2_REPO}/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar", - "${M2_REPO}/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar", - "${M2_REPO}/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar", - "${M2_REPO}/org/checkerframework/checker-qual/3.33.0/checker-qual-3.33.0.jar", - "${M2_REPO}/com/google/errorprone/error_prone_annotations/2.18.0/error_prone_annotations-2.18.0.jar", - "${M2_REPO}/com/google/j2objc/j2objc-annotations/2.8/j2objc-annotations-2.8.jar" - ], - "out": "${PROJECT_ROOT}/obp-commons/target/classes", - "classesDir": "${PROJECT_ROOT}/obp-commons/target/classes", - "resources": [ - "${PROJECT_ROOT}/obp-commons/src/main/resources" - ], - "scala": { - "organization": "org.scala-lang", - "name": "scala-compiler", - "version": "2.12.20", - "options": [ - "-unchecked", - "-explaintypes", - "-encoding", - "UTF-8", - "-feature" - ], - "jars": [ - "${M2_REPO}/org/scala-lang/scala-library/2.12.20/scala-library-2.12.20.jar", - "${M2_REPO}/org/scala-lang/scala-compiler/2.12.20/scala-compiler-2.12.20.jar", - "${M2_REPO}/org/scala-lang/scala-reflect/2.12.20/scala-reflect-2.12.20.jar" - ], - "analysis": "${PROJECT_ROOT}/obp-commons/target/bloop-bsp-clients-classes/classes-Metals-", - "setup": { - "order": "mixed", - "addLibraryToBootClasspath": true, - "addCompilerToClasspath": false, - "addExtraJarsToClasspath": false, - "manageBootClasspath": true, - "filterLibraryFromClasspath": true - } - }, - "java": { - "options": ["-source", "11", "-target", "11"] - }, - "platform": { - "name": "jvm", - "config": { - "home": "${JAVA_HOME}", - "options": [] - }, - "mainClass": [] - }, - "resolution": { - "modules": [] - }, - "tags": ["library"] - } -} -EOF - -# Generate obp-api.json -echo "🔨 Generating obp-api configuration..." -cat > "$PROJECT_ROOT/.bloop/obp-api.json" << EOF -{ - "version": "1.5.5", - "project": { - "name": "obp-api", - "directory": "${PROJECT_ROOT}/obp-api", - "workspaceDir": "${PROJECT_ROOT}", - "sources": [ - "${PROJECT_ROOT}/obp-api/src/main/scala", - "${PROJECT_ROOT}/obp-api/src/main/java" - ], - "dependencies": ["obp-commons"], - "classpath": [ - "${PROJECT_ROOT}/obp-api/target/classes", - "${PROJECT_ROOT}/obp-commons/target/classes", - "${M2_REPO}/com/tesobe/obp-commons/1.10.1/obp-commons-1.10.1.jar", - "${M2_REPO}/net/liftweb/lift-common_2.12/3.5.0/lift-common_2.12-3.5.0.jar", - "${M2_REPO}/org/scala-lang/scala-library/2.12.12/scala-library-2.12.12.jar", - "${M2_REPO}/org/slf4j/slf4j-api/1.7.32/slf4j-api-1.7.32.jar", - "${M2_REPO}/org/scala-lang/modules/scala-xml_2.12/1.3.0/scala-xml_2.12-1.3.0.jar", - "${M2_REPO}/net/liftweb/lift-util_2.12/3.5.0/lift-util_2.12-3.5.0.jar", - "${M2_REPO}/org/scala-lang/scala-compiler/2.12.12/scala-compiler-2.12.12.jar", - "${M2_REPO}/net/liftweb/lift-mapper_2.12/3.5.0/lift-mapper_2.12-3.5.0.jar", - "${M2_REPO}/net/liftweb/lift-json_2.12/3.5.0/lift-json_2.12-3.5.0.jar", - "${M2_REPO}/org/scala-lang/scala-reflect/2.12.20/scala-reflect-2.12.20.jar", - "${M2_REPO}/net/databinder/dispatch/dispatch-lift-json_2.12/0.13.1/dispatch-lift-json_2.12-0.13.1.jar", - "${M2_REPO}/ch/qos/logback/logback-classic/1.2.13/logback-classic-1.2.13.jar", - "${M2_REPO}/org/slf4j/log4j-over-slf4j/1.7.26/log4j-over-slf4j-1.7.26.jar", - "${M2_REPO}/org/slf4j/slf4j-ext/1.7.26/slf4j-ext-1.7.26.jar", - "${M2_REPO}/org/bouncycastle/bcpg-jdk15on/1.70/bcpg-jdk15on-1.70.jar", - "${M2_REPO}/org/bouncycastle/bcpkix-jdk15on/1.70/bcpkix-jdk15on-1.70.jar", - "${M2_REPO}/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar", - "${M2_REPO}/org/apache/commons/commons-text/1.10.0/commons-text-1.10.0.jar", - "${M2_REPO}/com/github/everit-org/json-schema/org.everit.json.schema/1.6.1/org.everit.json.schema-1.6.1.jar" - ], - "out": "${PROJECT_ROOT}/obp-api/target/classes", - "classesDir": "${PROJECT_ROOT}/obp-api/target/classes", - "resources": [ - "${PROJECT_ROOT}/obp-api/src/main/resources" - ], - "scala": { - "organization": "org.scala-lang", - "name": "scala-compiler", - "version": "2.12.20", - "options": [ - "-unchecked", - "-explaintypes", - "-encoding", - "UTF-8", - "-feature" - ], - "jars": [ - "${M2_REPO}/org/scala-lang/scala-library/2.12.20/scala-library-2.12.20.jar", - "${M2_REPO}/org/scala-lang/scala-compiler/2.12.20/scala-compiler-2.12.20.jar", - "${M2_REPO}/org/scala-lang/scala-reflect/2.12.20/scala-reflect-2.12.20.jar" - ], - "analysis": "${PROJECT_ROOT}/obp-api/target/bloop-bsp-clients-classes/classes-Metals-", - "setup": { - "order": "mixed", - "addLibraryToBootClasspath": true, - "addCompilerToClasspath": false, - "addExtraJarsToClasspath": false, - "manageBootClasspath": true, - "filterLibraryFromClasspath": true - } - }, - "java": { - "options": ["-source", "11", "-target", "11"] - }, - "platform": { - "name": "jvm", - "config": { - "home": "${JAVA_HOME}", - "options": [] - }, - "mainClass": [] - }, - "resolution": { - "modules": [] - }, - "tags": ["application"] - } -} -EOF - -echo -e "${GREEN}✅ Generated $PROJECT_ROOT/.bloop/obp-commons.json${NC}" -echo -e "${GREEN}✅ Generated $PROJECT_ROOT/.bloop/obp-api.json${NC}" - -# Verify the configurations -echo "🔍 Verifying generated configurations..." -if command -v bloop &> /dev/null; then - if bloop projects | grep -q "obp-api\|obp-commons"; then - echo -e "${GREEN}✅ Bloop can detect the projects${NC}" - else - echo -e "${YELLOW}⚠️ Bloop server may need to be restarted to detect new configurations${NC}" - echo "Run: pkill -f bloop && bloop server &" - fi -else - echo -e "${YELLOW}⚠️ Bloop not found, skipping verification${NC}" -fi - -echo "" -echo -e "${GREEN}🎉 Bloop configuration generation complete!${NC}" -echo "" -echo "📋 Next steps:" -echo "1. Restart Bloop server if needed: pkill -f bloop && bloop server &" -echo "2. Verify projects are detected: bloop projects" -echo "3. Test compilation: bloop compile obp-commons obp-api" -echo "4. Open project in Zed IDE for full language server support" -echo "" -echo -e "${GREEN}Happy coding! 🚀${NC}" diff --git a/zed/settings.json b/zed/settings.json deleted file mode 100644 index afafc18d5d..0000000000 --- a/zed/settings.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "format_on_save": "off", - "tab_size": 2, - "terminal": { - "env": { - "MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util.jar=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" - } - }, - "project_panel": { - "dock": "left", - "default_width": 300 - }, - "outline_panel": { - "dock": "right" - }, - "theme": "One Dark", - "ui_font_size": 14, - "buffer_font_size": 14, - "soft_wrap": "editor_width", - "show_whitespaces": "selection", - "tabs": { - "git_status": true, - "file_icons": true - }, - "gutter": { - "line_numbers": true - }, - "scrollbar": { - "show": "auto" - }, - "indent_guides": { - "enabled": true - }, - "lsp": { - "metals": { - "initialization_options": { - "compileOnSave": true, - "debuggingProvider": true, - "decorationProvider": true, - "didFocusProvider": true, - "doctorProvider": "html", - "executeClientCommandProvider": true, - "inputBoxProvider": true, - "quickPickProvider": true, - "renameProvider": true, - "statusBarProvider": "on", - "treeViewProvider": true, - "buildTool": "maven" - }, - "settings": { - "metals.ammoniteJvmProperties": ["-Xmx1G"], - "metals.buildServer.version": "2.0.0", - "metals.javaFormat.eclipseConfigPath": "", - "metals.javaFormat.eclipseProfile": "", - "metals.superMethodLensesEnabled": true, - "metals.testUserInterface": "Code Lenses", - "metals.bloopSbtAlreadyInstalled": true, - "metals.gradleScript": "", - "metals.mavenScript": "", - "metals.millScript": "", - "metals.sbtScript": "", - "metals.scalafmtConfigPath": ".scalafmt.conf", - "metals.enableSemanticHighlighting": true, - "metals.allowMultilineStringFormatting": true, - "metals.inlayHints.enabled": true, - "metals.inlayHints.hintsInPatternMatch.enabled": true, - "metals.inlayHints.implicitArguments.enabled": true, - "metals.inlayHints.implicitConversions.enabled": true, - "metals.inlayHints.inferredTypes.enabled": true, - "metals.inlayHints.typeParameters.enabled": true - } - } - }, - "languages": { - "Scala": { - "language_servers": ["metals"], - "format_on_save": "off" - } - } -} diff --git a/zed/setup-zed-ide.sh b/zed/setup-zed-ide.sh deleted file mode 100755 index 870345bc2b..0000000000 --- a/zed/setup-zed-ide.sh +++ /dev/null @@ -1,221 +0,0 @@ -#!/bin/bash - -# ZED IDE Complete Setup Script for OBP-API -# This script provides a unified setup for ZED IDE with full Scala language server support - -set -e - -echo "🚀 Setting up ZED IDE for OBP-API Scala development..." - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Get the project root directory (parent of zed folder) -PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -echo "📁 Project root: $PROJECT_ROOT" - -# Check if we're in the zed directory and project structure exists -if [[ ! -f "$PROJECT_ROOT/pom.xml" ]] || [[ ! -d "$PROJECT_ROOT/obp-api" ]] || [[ ! -d "$PROJECT_ROOT/obp-commons" ]]; then - echo -e "${RED}❌ Error: Could not find OBP-API project structure${NC}" - echo "Make sure you're running this from the zed/ folder of the OBP-API project" - exit 1 -fi - -# Change to project root for Maven operations -cd "$PROJECT_ROOT" - -echo "📁 Working directory: $(pwd)" - -# Check prerequisites -echo "🔍 Checking prerequisites..." - -# Check Java -if ! command -v java &> /dev/null; then - echo -e "${RED}❌ Java not found. Please install Java 11 or 17${NC}" - exit 1 -fi - -JAVA_VERSION=$(java -version 2>&1 | head -1 | cut -d'"' -f2 | cut -d'.' -f1-2) -echo -e "${GREEN}✅ Java found: ${JAVA_VERSION}${NC}" - -# Check Maven -if ! command -v mvn &> /dev/null; then - echo -e "${RED}❌ Maven not found. Please install Maven${NC}" - exit 1 -fi - -MVN_VERSION=$(mvn -version 2>&1 | head -1 | cut -d' ' -f3) -echo -e "${GREEN}✅ Maven found: ${MVN_VERSION}${NC}" - -# Check Coursier -if ! command -v cs &> /dev/null; then - echo -e "${YELLOW}⚠️ Coursier not found. Installing...${NC}" - curl -fL https://github.com/coursier/coursier/releases/latest/download/cs-x86_64-pc-linux.gz | gzip -d > cs - chmod +x cs - sudo mv cs /usr/local/bin/ - echo -e "${GREEN}✅ Coursier installed${NC}" -else - echo -e "${GREEN}✅ Coursier found${NC}" -fi - -# Check/Install Bloop -if ! command -v bloop &> /dev/null; then - echo -e "${YELLOW}⚠️ Bloop not found. Installing...${NC}" - cs install bloop - echo -e "${GREEN}✅ Bloop installed${NC}" -else - echo -e "${GREEN}✅ Bloop found: $(bloop about | head -1)${NC}" -fi - -# Start Bloop server if not running -if ! pgrep -f "bloop.*server" > /dev/null; then - echo "🔧 Starting Bloop server..." - bloop server & - sleep 3 - echo -e "${GREEN}✅ Bloop server started${NC}" -else - echo -e "${GREEN}✅ Bloop server already running${NC}" -fi - -# Compile the project to ensure dependencies are resolved -echo "🔨 Compiling Maven project (this may take a few minutes)..." -if mvn compile -q; then - echo -e "${GREEN}✅ Maven compilation successful${NC}" -else - echo -e "${RED}❌ Maven compilation failed. Please fix compilation errors first.${NC}" - exit 1 -fi - -# Copy ZED configuration files to project root -echo "📋 Setting up ZED IDE configuration..." -ZED_DIR="$PROJECT_ROOT/.zed" -ZED_SRC_DIR="$PROJECT_ROOT/zed" - -# Create .zed directory if it doesn't exist -if [ ! -d "$ZED_DIR" ]; then - echo "📁 Creating .zed directory..." - mkdir -p "$ZED_DIR" -else - echo "📁 .zed directory already exists" -fi - -# Copy settings.json -if [ -f "$ZED_SRC_DIR/settings.json" ]; then - echo "⚙️ Copying settings.json..." - cp "$ZED_SRC_DIR/settings.json" "$ZED_DIR/settings.json" - echo -e "${GREEN}✅ settings.json copied successfully${NC}" -else - echo -e "${RED}❌ Error: settings.json not found in zed folder${NC}" - exit 1 -fi - -# Copy tasks.json -if [ -f "$ZED_SRC_DIR/tasks.json" ]; then - echo "📋 Copying tasks.json..." - cp "$ZED_SRC_DIR/tasks.json" "$ZED_DIR/tasks.json" - echo -e "${GREEN}✅ tasks.json copied successfully${NC}" -else - echo -e "${RED}❌ Error: tasks.json not found in zed folder${NC}" - exit 1 -fi - -# Copy .metals-config.json if it exists -if [[ -f "$ZED_SRC_DIR/.metals-config.json" ]]; then - echo "🔧 Copying Metals configuration..." - cp "$ZED_SRC_DIR/.metals-config.json" "$PROJECT_ROOT/.metals-config.json" - echo -e "${GREEN}✅ Metals configuration copied${NC}" -fi - -echo -e "${GREEN}✅ ZED configuration files copied to .zed/ folder${NC}" - -# Generate Bloop configuration files dynamically -echo "🔧 Generating Bloop configuration files..." -if [[ -f "$ZED_SRC_DIR/generate-bloop-config.sh" ]]; then - chmod +x "$ZED_SRC_DIR/generate-bloop-config.sh" - "$ZED_SRC_DIR/generate-bloop-config.sh" - echo -e "${GREEN}✅ Bloop configuration files generated${NC}" -else - # Fallback: Check if existing configurations are present - if [[ -f "$PROJECT_ROOT/.bloop/obp-commons.json" && -f "$PROJECT_ROOT/.bloop/obp-api.json" ]]; then - echo -e "${GREEN}✅ Bloop configuration files already exist${NC}" - else - echo -e "${RED}❌ Bloop configuration files missing and generator not found.${NC}" - echo "Please ensure .bloop/*.json files exist or run zed/generate-bloop-config.sh manually" - exit 1 - fi -fi - -# Restart Bloop server to pick up new configurations -echo "🔄 Restarting Bloop server to detect new configurations..." -pkill -f bloop 2>/dev/null || true -sleep 1 -bloop server & -sleep 2 - -# Verify Bloop can see projects -echo "🔍 Verifying Bloop projects..." -BLOOP_PROJECTS=$(bloop projects 2>/dev/null || echo "") -if [[ "$BLOOP_PROJECTS" == *"obp-api"* && "$BLOOP_PROJECTS" == *"obp-commons"* ]]; then - echo -e "${GREEN}✅ Bloop projects detected:${NC}" - echo "$BLOOP_PROJECTS" | sed 's/^/ /' -else - echo -e "${YELLOW}⚠️ Bloop projects not immediately detected. This is normal for fresh setups.${NC}" - echo "The configuration should work when you open ZED IDE." -fi - -# Test Bloop compilation -echo "🧪 Testing Bloop compilation..." -if bloop compile obp-commons > /dev/null 2>&1; then - echo -e "${GREEN}✅ Bloop compilation test successful${NC}" -else - echo -e "${YELLOW}⚠️ Bloop compilation test failed, but setup is complete. Try restarting ZED IDE.${NC}" -fi - -# Check ZED configuration -if [[ -f "$PROJECT_ROOT/.zed/settings.json" ]]; then - echo -e "${GREEN}✅ ZED configuration found${NC}" -else - echo -e "${YELLOW}⚠️ ZED configuration not found in .zed/settings.json${NC}" -fi - -echo "" -echo -e "${GREEN}🎉 ZED IDE setup completed successfully!${NC}" -echo "" -echo "Your ZED configuration includes:" -echo " • Format on save: DISABLED (manual formatting only - use Ctrl+Shift+I)" -echo " • Scala/Metals LSP configuration optimized for OBP-API" -echo " • Pre-configured build and run tasks" -echo " • Dynamic Bloop configuration for language server support" -echo "" -echo "📋 Next steps:" -echo "1. Open ZED IDE" -echo "2. Open the OBP-API project directory in ZED" -echo "3. Wait for Metals to initialize (may take a few minutes)" -echo "4. Try 'Go to Definition' on a Scala symbol (F12 or Cmd+Click)" -echo "" -echo "🛠️ Available tasks (access with Cmd/Ctrl + Shift + P → 'task: spawn'):" -echo " • [1] Run OBP-API Server - Start development server" -echo " • [2] Test API Root Endpoint - Quick health check" -echo " • [3] Compile Only - Fast syntax check" -echo " • [4] Clean Target Folders - Remove build artifacts" -echo " • Quick Build Dependencies - Build deps only (for onboarding)" -echo " • Run Tests - Execute full test suite" -echo "" -echo "💡 Troubleshooting:" -echo "• If 'Go to Definition' doesn't work immediately, restart ZED IDE" -echo "• Use 'ZED: Reload Window' from the command palette if needed" -echo "• Check zed/README.md for comprehensive documentation" -echo "• Run './zed/generate-bloop-config.sh' to regenerate configurations if needed" -echo "" -echo "🔗 Resources:" -echo "• Complete ZED setup guide: zed/README.md" -echo "• Bloop projects: bloop projects" -echo "• Bloop compilation: bloop compile obp-commons obp-api" -echo "" -echo "Note: The .zed folder is in .gitignore, so you can customize settings" -echo " without affecting other developers." -echo "" -echo -e "${GREEN}Happy coding! 🚀${NC}" diff --git a/zed/setup-zed.bat b/zed/setup-zed.bat deleted file mode 100644 index 303e49d8eb..0000000000 --- a/zed/setup-zed.bat +++ /dev/null @@ -1,64 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -REM Zed IDE Setup Script for OBP-API (Windows) -REM This script copies the recommended Zed configuration to your local .zed folder - -echo 🔧 Setting up Zed IDE configuration for OBP-API... - -set "SCRIPT_DIR=%~dp0" -set "PROJECT_ROOT=%SCRIPT_DIR%.." -set "ZED_DIR=%PROJECT_ROOT%\.zed" - -REM Create .zed directory if it doesn't exist -if not exist "%ZED_DIR%" ( - echo 📁 Creating .zed directory... - mkdir "%ZED_DIR%" -) else ( - echo 📁 .zed directory already exists -) - -REM Copy settings.json -if exist "%SCRIPT_DIR%settings.json" ( - echo ⚙️ Copying settings.json... - copy "%SCRIPT_DIR%settings.json" "%ZED_DIR%\settings.json" >nul - if !errorlevel! equ 0 ( - echo ✅ settings.json copied successfully - ) else ( - echo ❌ Error copying settings.json - exit /b 1 - ) -) else ( - echo ❌ Error: settings.json not found in zed folder - exit /b 1 -) - -REM Copy tasks.json -if exist "%SCRIPT_DIR%tasks.json" ( - echo 📋 Copying tasks.json... - copy "%SCRIPT_DIR%tasks.json" "%ZED_DIR%\tasks.json" >nul - if !errorlevel! equ 0 ( - echo ✅ tasks.json copied successfully - ) else ( - echo ❌ Error copying tasks.json - exit /b 1 - ) -) else ( - echo ❌ Error: tasks.json not found in zed folder - exit /b 1 -) - -echo. -echo 🎉 Zed IDE setup completed successfully! -echo. -echo Your Zed configuration includes: -echo • Format on save: DISABLED (preserves your code formatting) -echo • Scala/Metals LSP configuration optimized for OBP-API -echo • 9 predefined tasks for building, running, and testing -echo. -echo To see available tasks in Zed, use: Ctrl + Shift + P → 'task: spawn' -echo. -echo Note: The .zed folder is in .gitignore, so you can customize settings -echo without affecting other developers. - -pause \ No newline at end of file diff --git a/zed/tasks.json b/zed/tasks.json deleted file mode 100644 index 236c2f7975..0000000000 --- a/zed/tasks.json +++ /dev/null @@ -1,111 +0,0 @@ -[ - { - "label": "[1] Run OBP-API Server", - "command": "mvn", - "args": ["jetty:run", "-pl", "obp-api"], - "env": { - "MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED --add-opens=java.base/java.util.stream=ALL-UNNAMED --add-opens=java.base/java.util.regex=ALL-UNNAMED" - }, - "use_new_terminal": true, - "allow_concurrent_runs": false, - "reveal": "always", - "tags": ["run", "server"] - }, - { - "label": "[2] Test API Root Endpoint", - "command": "curl", - "args": [ - "-X", - "GET", - "http://localhost:8080/obp/v5.1.0/root", - "-H", - "accept: application/json" - ], - "use_new_terminal": false, - "allow_concurrent_runs": true, - "reveal": "always", - "tags": ["test", "api"] - }, - { - "label": "[3] Compile Only", - "command": "mvn", - "args": ["compile", "-pl", "obp-api"], - "env": { - "MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED" - }, - "use_new_terminal": false, - "allow_concurrent_runs": false, - "reveal": "always", - "tags": ["compile", "build"] - }, - { - "label": "[4] Build OBP-API", - "command": "mvn", - "args": [ - "install", - "-pl", - ".,obp-commons", - "-am", - "-DskipTests", - "-Ddependency-check.skip=true" - ], - "env": { - "MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED" - }, - "use_new_terminal": false, - "allow_concurrent_runs": false, - "reveal": "always", - "tags": ["build"] - }, - { - "label": "[5] Clean Target Folders", - "command": "mvn", - "args": ["clean"], - "use_new_terminal": false, - "allow_concurrent_runs": false, - "reveal": "always", - "tags": ["clean", "build"] - }, - { - "label": "[6] Kill OBP-APIServer on Port 8080", - "command": "bash", - "args": [ - "-c", - "lsof -ti:8080 | xargs kill -9 || echo 'No process found on port 8080'" - ], - "use_new_terminal": false, - "allow_concurrent_runs": true, - "reveal": "always", - "tags": ["utility"] - }, - { - "label": "[7] Run Tests", - "command": "mvn", - "args": ["test", "-pl", "obp-api"], - "env": { - "MAVEN_OPTS": "-Xss128m --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED" - }, - "use_new_terminal": false, - "allow_concurrent_runs": false, - "reveal": "always", - "tags": ["test"] - }, - { - "label": "[8] Maven Validate", - "command": "mvn", - "args": ["validate"], - "use_new_terminal": false, - "allow_concurrent_runs": false, - "reveal": "always", - "tags": ["validate"] - }, - { - "label": "[9] Check Dependencies", - "command": "mvn", - "args": ["dependency:resolve"], - "use_new_terminal": false, - "allow_concurrent_runs": false, - "reveal": "always", - "tags": ["dependencies"] - } -]