diff --git a/.github/workflows/jersey-main.yml b/.github/workflows/jersey-main.yml new file mode 100644 index 000000000..fc3545112 --- /dev/null +++ b/.github/workflows/jersey-main.yml @@ -0,0 +1,148 @@ +name: CI jersey-main +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +env: + REGISTRY: ghcr.io + REPOSITORY: ${{ github.repository }} + IMAGE_NAME: radar-appserver + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Compile code + run: ./gradlew :appserver-jersey:assemble + + - name: Check + run: ./gradlew :appserver-jersey:check + + # Check that the docker image builds correctly + docker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v5 + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Lowercase image name + run: | + echo "DOCKER_IMAGE=${REGISTRY}/${REPOSITORY,,}/${IMAGE_NAME}" >>${GITHUB_ENV} + + # Add Docker labels and tags + - name: Docker meta + id: docker_meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.DOCKER_IMAGE }} + + # Setup docker build environment + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + id: cache-buildx + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile.appserver-jersey', 'appserver-jersey/build.gradle.kts', 'buildSrc/src/main/kotlin/Versions.kt', 'settings.gradle.kts', 'build.gradle.kts', 'appserver-jersey/src/main/**') }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Cache parameters + id: cache-parameters + run: | + if [ "${{ steps.cache-buildx.outputs.cache-hit }}" = "true" ]; then + echo "cache-to=" >> $GITHUB_OUTPUT + else + echo "cache-to=type=local,dest=/tmp/.buildx-cache-new,mode=max" >> $GITHUB_OUTPUT + fi + + - name: Build docker + uses: docker/build-push-action@v3 + with: + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: ${{ steps.cache-parameters.outputs.cache-to }} + load: true + context: . + file: Dockerfile.appserver-jersey + tags: ${{ steps.docker_meta.outputs.tags }} + labels: | + ${{ steps.docker_meta.outputs.labels }} + org.opencontainers.image.vendor=RADAR-base + org.opencontainers.image.licenses=Apache-2.0 + + - name: Inspect docker image + run: docker image inspect ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} + + - name: Check docker image + run: docker run --rm ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} curl --help + + # Push the image on the dev and master branches + - name: Push image + if: ${{ github.event_name != 'pull_request' }} + run: docker push ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} + + - name: Move docker build cache + if: steps.cache-buildx.outputs.cache-hit != 'true' + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Install gpg secret key + run: | + cat <(echo -e "${{ secrets.GPG_SECRET_KEY }}") | gpg --batch --import + gpg --list-secret-keys --keyid-format LONG + + - name: Decrypt google application credentials + run: | + gpg --pinentry-mode loopback --local-user "Adi Mishra" --batch --yes --passphrase "${{ secrets.GPG_SECRET_KEY_PASSPHRASE }}" --output appserver-jersey/src/integrationTest/resources/docker/fcm/google-credentials.json --decrypt appserver-jersey/src/integrationTest/resources/docker/fcm/google-credentials.enc.gpg + + - name: Integration test + run: | + echo "RADAR_APPSERVER_IMAGE_NAME=${{ env.DOCKER_IMAGE }}" >> appserver-jersey/src/integrationTest/resources/docker/.env + echo "RADAR_APPSERVER_TAG=${{ steps.docker_meta.outputs.version }}" >> appserver-jersey/src/integrationTest/resources/docker/.env + ./gradlew :appserver-jersey:composeUp -PdockerComposeBuild=false + sleep 15 + ./gradlew :appserver-jersey:integrationTest -PdockerComposeBuild=false + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: integration-test-logs + path: appserver-jersey/build/container-logs/ + retention-days: 7 diff --git a/.github/workflows/jersey-release.yml b/.github/workflows/jersey-release.yml new file mode 100644 index 000000000..df85e0269 --- /dev/null +++ b/.github/workflows/jersey-release.yml @@ -0,0 +1,88 @@ +name: Appserver Jersey Release + +on: + release: + types: [published] + +env: + REGISTRY: ghcr.io + REPOSITORY: ${{ github.repository }} + IMAGE_NAME: radar-appserver + +jobs: + upload: + runs-on: ubuntu-latest + permissions: write-all + + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Compile code + run: ./gradlew assemble + + - name: Upload to GitHub + uses: AButler/upload-release-assets@v3.0 + with: + files: 'appserver-jersey/build/libs/*;appserver-jersey/build/distributions/*' + repo-token: ${{ secrets.GITHUB_TOKEN }} + + docker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Lowercase image name + run: | + echo "DOCKER_IMAGE=${REGISTRY}/${REPOSITORY,,}/${IMAGE_NAME}" >>${GITHUB_ENV} + + # Add Docker labels and tags + - name: Docker meta + id: docker_meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.DOCKER_IMAGE }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Build docker + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + context: . + push: true + tags: ${{ steps.docker_meta.outputs.tags }} + labels: | + ${{ steps.docker_meta.outputs.labels }} + org.opencontainers.image.vendor=RADAR-base + org.opencontainers.image.licenses=Apache-2.0 + + - name: Inspect docker image + run: | + docker pull ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} + docker image inspect ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 5c2fd1ee9..000000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,189 +0,0 @@ -# Continuous integration, including test and integration test -name: CI - -# Run in master and dev branches and in all pull requests to those branches, as well as on workflow dispatch for downstream testing -on: - workflow_dispatch: - push: - branches: [ master, dev ] - pull_request: - branches: [ master, dev ] - -env: - DOCKER_IMAGE: radarbase/radar-appserver - -jobs: - # Build and test the code - build: - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 - - - uses: actions/setup-java@v3 - with: - java-version: 17 - distribution: temurin - - - uses: gradle/gradle-build-action@v2 - - # Compile the code - - name: Compile code - run: ./gradlew assemble - - # Use 'docker compose' instead of 'docker-compose' to use v2 - - name: Setup docker services - run: | - sudo mkdir -p /usr/local/var/lib/radar/appserver/logs/ - sudo chown -R $(whoami) /usr/local/var/lib/radar/appserver/logs - docker compose -f src/integrationTest/resources/docker/non_appserver/docker-compose.yml up -d - # Wait for services to start up. - sleep 50 - - - name: Install gpg secret key - run: | - cat <(echo -e "${{ secrets.GPG_SECRET_KEY }}") | gpg --batch --import - gpg --list-secret-keys --keyid-format LONG - - name: Decrypt google application credentials - run: | - gpg --pinentry-mode loopback --local-user "Yatharth Ranjan" --batch --yes --passphrase "${{ secrets.GPG_SECRET_KEY_PASSPHRASE }}" --output src/integrationTest/resources/google-credentials.json --decrypt src/integrationTest/resources/google-credentials.enc.gpg - - # Gradle check - - name: Check - run: GOOGLE_APPLICATION_CREDENTIALS=$(pwd)/src/integrationTest/resources/google-credentials.json ./gradlew check - - - name: Upload build artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - path: build/reports - if-no-files-found: ignore - retention-days: 5 - - # Build and test the code against the :dev docker image of parent repositories - test-downstream: - # The type of runner that the job will run on - runs-on: ubuntu-latest - # only run this on 'ready for review' PRs or when triggered by an upstream job - if: github.event.pull_request.draft == false || github.event_name == 'workflow_dispatch' - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 - - - uses: actions/setup-java@v3 - with: - java-version: 17 - distribution: temurin - - # Use 'docker compose' instead of 'docker-compose' to use v2 - - name: Setup docker services (:dev) - run: | - sudo mkdir -p /usr/local/var/lib/radar/appserver/logs/ - sudo chown -R $(whoami) /usr/local/var/lib/radar/appserver/logs - # call docker compose without args to include the override file - cd src/integrationTest/resources/docker/appserver_downstream - docker compose up -d - # Wait for services to start up. - sleep 50 - - - name: Install gpg secret key - run: | - cat <(echo -e "${{ secrets.GPG_SECRET_KEY }}") | gpg --batch --import - gpg --list-secret-keys --keyid-format LONG - - - name: Decrypt google application credentials - run: | - gpg --pinentry-mode loopback --local-user "Yatharth Ranjan" --batch --yes --passphrase "${{ secrets.GPG_SECRET_KEY_PASSPHRASE }}" --output src/integrationTest/resources/google-credentials.json --decrypt src/integrationTest/resources/google-credentials.enc.gpg - - # Gradle check - - name: Check - run: GOOGLE_APPLICATION_CREDENTIALS=$(pwd)/src/integrationTest/resources/google-credentials.json ./gradlew check - - # Check that the docker image builds correctly - docker: - # The type of runner that the job will run on - runs-on: ubuntu-latest - if: github.event_name != 'workflow_dispatch' - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Add Docker labels and tags - - name: Docker meta - id: docker_meta - uses: docker/metadata-action@v4 - with: - images: ${{ env.DOCKER_IMAGE }} - - # Setup docker build environment - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Cache Docker layers - id: cache-buildx - uses: actions/cache@v3 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile', '**/*.gradle.kts', 'gradle.properties', 'src/main/**') }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Cache parameters - id: cache-parameters - run: | - if [ "${{ steps.cache-buildx.outputs.cache-hit }}" = "true" ]; then - echo "::set-output name=cache-to::" - else - echo "::set-output name=cache-to::type=local,dest=/tmp/.buildx-cache-new,mode=max" - fi - - - name: Build docker - uses: docker/build-push-action@v3 - with: - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: ${{ steps.cache-parameters.outputs.cache-to }} - load: true - tags: ${{ steps.docker_meta.outputs.tags }} - # Use runtime labels from docker_meta as well as fixed labels - labels: | - ${{ steps.docker_meta.outputs.labels }} - maintainer=Yatharth Ranjan , Pauline Conde - org.opencontainers.image.authors=Yatharth Ranjan , Pauline Conde - org.opencontainers.image.vendor=RADAR-base - org.opencontainers.image.licenses=Apache-2.0 - - - name: Inspect docker image - run: docker image inspect ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} - - # Push the image on the dev and master branches - - name: Push image - if: ${{ github.event_name != 'pull_request' }} - run: docker push ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} - - # Temp fix - # https://github.com/docker/build-push-action/issues/252 - # https://github.com/moby/buildkit/issues/1896 - - name: Move docker build cache - if: steps.cache-buildx.outputs.cache-hit != 'true' - run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache - - - diff --git a/.github/workflows/microservices-main.yml b/.github/workflows/microservices-main.yml new file mode 100644 index 000000000..c1e201594 --- /dev/null +++ b/.github/workflows/microservices-main.yml @@ -0,0 +1,251 @@ +name: CI microservices-main +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +jobs: + build: + name: Build ${{ matrix.service }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + service: + - project-service + - user-service + - gateway-service + - github-service + - protocol-service + - task-service + - cloud-messaging-service + + steps: + - name: Perform checkout + uses: actions/checkout@v5 + + - name: Setup Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Compile code + run: ./gradlew :microservices:${{ matrix.service }}:assemble + + - name: Check + run: ./gradlew :microservices:${{ matrix.service }}:check + + docker: + name: Docker setup for ${{ matrix.service }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + strategy: + fail-fast: false + matrix: + service: + - project-service + - user-service + - gateway-service + - protocol-service + - github-service + - task-service + - cloud-messaging-service + env: + REGISTRY: ghcr.io + REPOSITORY: ${{ github.repository }} + + steps: + - name: Perform checkout + uses: actions/checkout@v5 + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Login to Container Registry (${{ env.REGISTRY }}) + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Lowercase image name + run: | + MODULE=${{ matrix.service }} + IMAGE_NAME=${MODULE} + echo "DOCKER_IMAGE=${REGISTRY}/${REPOSITORY,,}/${IMAGE_NAME}" >>${GITHUB_ENV} + + - name: Docker meta + id: docker_meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.DOCKER_IMAGE }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + id: cache-buildx + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ hashFiles( + format('microservices/{0}/Dockerfile', matrix.service), + 'settings.gradle.kts', + 'buildSrc/src/main/kotlin/Versions.kt', + 'build.gradle.kts', + format('microservices/{0}/build.gradle.kts', matrix.service), + format('microservices/{0}/src/main/**', matrix.service) + ) }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Cache parameters + id: cache-parameters + run: | + if [ "${{ steps.cache-buildx.outputs.cache-hit }}" = "true" ]; then + echo "cache-to=" >> $GITHUB_OUTPUT + else + echo "cache-to=type=local,dest=/tmp/.buildx-cache-new,mode=max" >> $GITHUB_OUTPUT + fi + + - name: Compile ${{ matrix.service }} code + run: ./gradlew --no-daemon :microservices:${{ matrix.service }}:assemble + + - name: Build docker + uses: docker/build-push-action@v3 + with: + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: ${{ steps.cache-parameters.outputs.cache-to }} + load: true + context: . + file: microservices/${{ matrix.service }}/Dockerfile + tags: ${{ steps.docker_meta.outputs.tags }} + labels: | + ${{ steps.docker_meta.outputs.labels }} + org.opencontainers.image.vendor=RADAR-base + org.opencontainers.image.licenses=Apache-2.0 + + - name: Inspect docker image + run: docker image inspect ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} + + - name: Check docker image + run: docker run --rm ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} curl --help || true + + - name: Push image + if: ${{ github.event_name != 'pull_request' }} + run: docker push ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} + + - name: Move docker build cache + if: steps.cache-buildx.outputs.cache-hit != 'true' + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache || true + + - name: Write image metadata file + run: | + mkdir -p image-meta + cat > image-meta/${{ matrix.service }}.json <> $OUTFILE + echo "RADAR_APPSERVER_${svc_upper}_IMAGE_TAG=${tag}" >> $OUTFILE + + echo "Wrote RADAR_APPSERVER_${svc_upper}_IMAGE_NAME=${image}" + done + + cat $OUTFILE + + - name: Install gpg secret key + run: | + cat <(echo -e "${{ secrets.GPG_SECRET_KEY }}") | gpg --batch --import || true + gpg --list-secret-keys --keyid-format LONG || true + + - name: Decrypt google application credentials + run: | + # adjust path to encrypted file if needed + gpg --pinentry-mode loopback --local-user "Adi Mishra" --batch --yes \ + --passphrase "${{ secrets.GPG_SECRET_KEY_PASSPHRASE }}" \ + --output microservices/integration-tests/src/integrationTest/resources/docker/fcm/google-credentials.json \ + --decrypt microservices/integration-tests/src/integrationTest/resources/docker/fcm/google-credentials.enc.gpg + + - name: Integration test + run: | + ./gradlew :microservices:integration-tests:composeUp -PdockerComposeBuild=false + sleep 15 + ./gradlew :microservices:integration-tests:integrationTest -PdockerComposeBuild=false + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: integration-test-logs + path: appserver-jersey/build/container-logs/ + retention-days: 7 diff --git a/.github/workflows/microservices-release.yml b/.github/workflows/microservices-release.yml new file mode 100644 index 000000000..7340b46ee --- /dev/null +++ b/.github/workflows/microservices-release.yml @@ -0,0 +1,124 @@ +name: Appserver Jersey Release + +on: + release: + types: [published] + +env: + REGISTRY: ghcr.io + REPOSITORY: ${{ github.repository }} + IMAGE_NAME: radar-appserver + +jobs: + upload: + runs-on: ubuntu-latest + permissions: write-all + + strategy: + fail-fast: false + matrix: + service: + - project-service + - user-service + - gateway-service + - github-service + - protocol-service + - task-service + - cloud-messaging-service + + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Compile ${{ matrix.service }} code + run: ./gradlew --no-daemon :microservices:${{ matrix.service }}:assemble + + - name: Upload to GitHub + uses: AButler/upload-release-assets@v3.0 + with: + files: 'microservices/${{ matrix.service }}/build/libs/*;microservices/${{ matrix.service }}/build/distributions/*' + repo-token: ${{ secrets.GITHUB_TOKEN }} + + docker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + strategy: + fail-fast: false + matrix: + service: + - project-service + - user-service + - gateway-service + - protocol-service + - github-service + - task-service + - cloud-messaging-service + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Lowercase image name + run: | + echo "DOCKER_IMAGE=${REGISTRY}/${REPOSITORY,,}/${IMAGE_NAME}" >>${GITHUB_ENV} + + # Add Docker labels and tags + - name: Docker meta + id: docker_meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.DOCKER_IMAGE }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Compile ${{ matrix.service }} code + run: ./gradlew --no-daemon :microservices:${{ matrix.service }}:assemble + + - name: Build docker + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + context: . + push: true + file: microservices/${{ matrix.service }}/Dockerfile + tags: ${{ steps.docker_meta.outputs.tags }} + labels: | + ${{ steps.docker_meta.outputs.labels }} + org.opencontainers.image.vendor=RADAR-base + org.opencontainers.image.licenses=Apache-2.0 + + - name: Inspect docker image + run: | + docker pull ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} + docker image inspect ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 07b7df002..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,86 +0,0 @@ -# Create release files -name: Release - -on: - release: - types: [published] - -jobs: - upload: - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 - with: - java-version: 17 - distribution: temurin - - - uses: gradle/gradle-build-action@v2 - - # Compile code - - name: Compile code - run: ./gradlew assemble - - # Upload it to GitHub - - name: Upload to GitHub - uses: AButler/upload-release-assets@v2.0.2 - with: - files: 'build/libs/*' - repo-token: ${{ secrets.GITHUB_TOKEN }} - - # Build and push tagged release docker image - docker: - # The type of runner that the job will run on - runs-on: ubuntu-latest - - env: - DOCKER_IMAGE: radarbase/radar-appserver - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - uses: actions/checkout@v3 - - # Setup docker build environment - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Add Docker labels and tags - - name: Docker meta - id: docker_meta - uses: docker/metadata-action@v4 - with: - images: ${{ env.DOCKER_IMAGE }} - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - - name: Build docker - uses: docker/build-push-action@v3 - with: - # Allow running the image on the architectures supported by openjdk:17-jre-slim - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.docker_meta.outputs.tags }} - # Use runtime labels from docker_meta as well as fixed labels - labels: | - ${{ steps.docker_meta.outputs.labels }} - maintainer=Yatharth Ranjan , Pauline Conde - org.opencontainers.image.authors=Yatharth Ranjan , Pauline Conde - org.opencontainers.image.vendor=RADAR-base - org.opencontainers.image.licenses=Apache-2.0 - - name: Inspect docker image - run: | - docker pull ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} - docker image inspect ${{ env.DOCKER_IMAGE }}:${{ steps.docker_meta.outputs.version }} diff --git a/appserver-jersey/src/integrationTest/resources/docker/.env b/appserver-jersey/src/integrationTest/resources/docker/.env index 670ebf09e..9d127183b 100644 --- a/appserver-jersey/src/integrationTest/resources/docker/.env +++ b/appserver-jersey/src/integrationTest/resources/docker/.env @@ -1 +1,2 @@ RADAR_APPSERVER_TAG=SNAPSHOT +RADAR_APPSERVER_IMAGE_NAME=ghcr.io/radar-base/radar-appserver/radar-appserver diff --git a/appserver-jersey/src/integrationTest/resources/docker/docker-compose.yml b/appserver-jersey/src/integrationTest/resources/docker/docker-compose.yml index 2648e2966..9ff8d72d5 100644 --- a/appserver-jersey/src/integrationTest/resources/docker/docker-compose.yml +++ b/appserver-jersey/src/integrationTest/resources/docker/docker-compose.yml @@ -54,7 +54,7 @@ services: build: context: ../../../../../ dockerfile: Dockerfile.appserver-jersey - image: radarbase/radar-appserver:${RADAR_APPSERVER_TAG} + image: ${RADAR_APPSERVER_IMAGE_NAME}:${RADAR_APPSERVER_TAG} depends_on: - managementportal - appserver-postgres @@ -68,6 +68,6 @@ services: APPSERVER_JDBC_DRIVER: org.postgresql.Driver APPSERVER_HIBERNATE_DIALECT: org.hibernate.dialect.PostgreSQLDialect MANAGEMENTPORTAL_BASE_URL: http://managementportal:8081/managementportal - GOOGLE_APPLICATION_CREDENTIALS: /appserver-includes/google-application-credentials.json + GOOGLE_APPLICATION_CREDENTIALS: /appserver-includes/google-credentials.json volumes: - ./fcm:/appserver-includes/ diff --git a/appserver-jersey/src/integrationTest/resources/docker/fcm/google-credentials.enc.gpg b/appserver-jersey/src/integrationTest/resources/docker/fcm/google-credentials.enc.gpg new file mode 100644 index 000000000..e83aaffb1 Binary files /dev/null and b/appserver-jersey/src/integrationTest/resources/docker/fcm/google-credentials.enc.gpg differ diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/AppserverConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/AppserverConfig.kt index 6d2570eed..db52b2c0e 100644 --- a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/AppserverConfig.kt +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/AppserverConfig.kt @@ -52,6 +52,14 @@ data class AppserverConfig( }, ) .copyOnChange( + github, + { + it.withEnv() + }, + { + copy(github = it) + }, + ).copyOnChange( email, { it.withEnv() diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubClientConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubClientConfig.kt index 5fafb58d8..4b97053df 100644 --- a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubClientConfig.kt +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubClientConfig.kt @@ -17,10 +17,16 @@ package org.radarbase.appserver.jersey.config.github import com.fasterxml.jackson.annotation.JsonProperty +import org.radarbase.jersey.config.ConfigLoader.copyEnv data class GithubClientConfig( val maxContentLength: Long = 10_00_000, @field:JsonProperty("timeoutSec") val timeout: Long = 10L, val githubToken: String? = null, -) +) { + fun withEnv() = this. + copyEnv("SECURITY_GITHUB_CLIENT_TOKEN") { + copy(githubToken = it) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubConfig.kt index a857b2acb..4c534a056 100644 --- a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubConfig.kt +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubConfig.kt @@ -16,7 +16,17 @@ package org.radarbase.appserver.jersey.config.github +import org.radarbase.jersey.config.ConfigLoader.copyOnChange + data class GithubConfig( val cache: GithubCacheConfig = GithubCacheConfig(), val client: GithubClientConfig = GithubClientConfig(), -) +) { + fun withEnv() = this.copyOnChange( + client, + { it.withEnv() }, + { + copy(client = it) + }, + ) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/FcmNotificationResource.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/FcmNotificationResource.kt index 26952c088..683ecf749 100644 --- a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/FcmNotificationResource.kt +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/FcmNotificationResource.kt @@ -158,9 +158,7 @@ class FcmNotificationResource @Inject constructor( @Suspended asyncResponse: AsyncResponse, ) { asyncService.runAsCoroutine(asyncResponse, requestTimeout) { - log.info("Processing request....") val token = tokenForCurrentRequest(asyncService, tokenProvider) - log.info("Checking permissions for subject ${token.subject} in project $projectId") authService.checkPermission( Permission.SUBJECT_READ, EntityDetails(project = projectId, subject = token.subject), diff --git a/microservices/cloud-messaging-service/build.gradle.kts b/microservices/cloud-messaging-service/build.gradle.kts index 6897a7dd4..a6f7e22d5 100644 --- a/microservices/cloud-messaging-service/build.gradle.kts +++ b/microservices/cloud-messaging-service/build.gradle.kts @@ -18,3 +18,8 @@ dependencies { implementation(project(":microservices:core")) implementation(project(":microservices:contract")) } + +ktlint { + ignoreFailures.set(true) + outputColorName.set("RED") +} diff --git a/microservices/cloud-messaging-service/src/main/kotlin/org/radarbase/appserver/microservices/cloud/messaging/service/transmitter/FcmTransmitter.kt b/microservices/cloud-messaging-service/src/main/kotlin/org/radarbase/appserver/microservices/cloud/messaging/service/transmitter/FcmTransmitter.kt index 68e75bfc3..d96594245 100644 --- a/microservices/cloud-messaging-service/src/main/kotlin/org/radarbase/appserver/microservices/cloud/messaging/service/transmitter/FcmTransmitter.kt +++ b/microservices/cloud-messaging-service/src/main/kotlin/org/radarbase/appserver/microservices/cloud/messaging/service/transmitter/FcmTransmitter.kt @@ -43,7 +43,7 @@ import java.util.Objects class FcmTransmitter @Inject constructor( private val fcmSender: FcmSender, private val notificationService: FcmNotificationService, -// private val dataMessageService: FcmDataMessageService, + private val dataMessageService: FcmDataMessageService, config: CloudMessagingServiceConfig, ) : DataMessageTransmitter, NotificationTransmitter { @@ -106,7 +106,7 @@ class FcmTransmitter @Inject constructor( } } - private suspend fun handleFCMErrorCode(errorCode: MessagingErrorCode, message: Message) { + private suspend fun handleFCMErrorCode(errorCode: MessagingErrorCode?, message: Message) { when (errorCode) { MessagingErrorCode.INTERNAL, MessagingErrorCode.QUOTA_EXCEEDED, MessagingErrorCode.INVALID_ARGUMENT, MessagingErrorCode.SENDER_ID_MISMATCH, MessagingErrorCode.THIRD_PARTY_AUTH_ERROR -> {} MessagingErrorCode.UNAVAILABLE -> { @@ -123,10 +123,10 @@ class FcmTransmitter @Inject constructor( projectId, subjectId, ) -// dataMessageService.removeDataMessagesForUser( -// projectId, -// subjectId, -// ) + dataMessageService.removeDataMessagesForUser( + projectId, + subjectId, + ) val userId = requireNotNullField(message.userId, "Notification's UserId") val user = deserializeDtoFromContract( @@ -149,6 +149,7 @@ class FcmTransmitter @Inject constructor( } } } + else -> logger.error("Unknown error occurred when transmitting message") } } diff --git a/microservices/contract/build.gradle.kts b/microservices/contract/build.gradle.kts index df54b8be5..9016e554e 100644 --- a/microservices/contract/build.gradle.kts +++ b/microservices/contract/build.gradle.kts @@ -13,3 +13,8 @@ dependencies { implementation("io.ktor:ktor-client-content-negotiation:${Versions.ktorVersion}") implementation("io.ktor:ktor-serialization-kotlinx-json:${Versions.ktorVersion}") } + +ktlint { + ignoreFailures.set(true) + outputColorName.set("RED") +} diff --git a/microservices/core/build.gradle.kts b/microservices/core/build.gradle.kts index ecfe3ad81..839608db3 100644 --- a/microservices/core/build.gradle.kts +++ b/microservices/core/build.gradle.kts @@ -45,3 +45,8 @@ allOpen { annotation("jakarta.persistence.Entity") annotation("jakarta.persistence.Embeddable") } + +ktlint { + ignoreFailures.set(true) + outputColorName.set("RED") +} diff --git a/microservices/core/src/main/kotlin/org/radarbase/appserver/microservices/core/serialization/InstantSerializer.kt b/microservices/core/src/main/kotlin/org/radarbase/appserver/microservices/core/serialization/InstantSerializer.kt index 5949103f1..6692c8e28 100644 --- a/microservices/core/src/main/kotlin/org/radarbase/appserver/microservices/core/serialization/InstantSerializer.kt +++ b/microservices/core/src/main/kotlin/org/radarbase/appserver/microservices/core/serialization/InstantSerializer.kt @@ -27,6 +27,7 @@ import kotlinx.serialization.json.double import kotlinx.serialization.json.doubleOrNull import kotlinx.serialization.json.jsonPrimitive import java.time.Instant +import java.time.temporal.ChronoUnit import kotlin.math.floor object InstantSerializer : KSerializer { @@ -50,6 +51,7 @@ object InstantSerializer : KSerializer { } override fun serialize(encoder: Encoder, value: Instant) { - encoder.encodeString(value.toString()) + val truncated = value.truncatedTo(ChronoUnit.MILLIS) + encoder.encodeString(truncated.toString()) } } diff --git a/microservices/docker-compose.yml b/microservices/docker-compose.yml index 79d0d197c..8875749f6 100644 --- a/microservices/docker-compose.yml +++ b/microservices/docker-compose.yml @@ -63,7 +63,7 @@ services: ports: - "8081:8081" volumes: - - ./resources/docker/etc/:/mp-includes/ + - ./integration-tests/src/integrationTest/resources/docker/etc/:/mp-includes/ #---------------------------------------------------------------------------# # Gateway Service # @@ -72,7 +72,7 @@ services: build: context: ../ dockerfile: microservices/gateway-service/Dockerfile - image: radarbase/appserver-gateway-service:test + image: radarbase/appserver-gateway-service:SNAPSHOT ports: - "8080:8080" networks: @@ -104,7 +104,7 @@ services: build: context: .. dockerfile: microservices/project-service/Dockerfile - image: radarbase/appserver-project-service:test + image: radarbase/appserver-project-service:SNAPSHOT networks: - microservice - project-service-internal @@ -133,7 +133,7 @@ services: build: context: .. dockerfile: microservices/user-service/Dockerfile - image: radarbase/appserver-user-service:test + image: radarbase/appserver-user-service:SNAPSHOT networks: - microservice - user-service-internal @@ -153,7 +153,7 @@ services: build: context: .. dockerfile: microservices/github-service/Dockerfile - image: radarbase/appserver-github-service:test + image: radarbase/appserver-github-service:SNAPSHOT networks: - microservice - public @@ -165,7 +165,7 @@ services: build: context: .. dockerfile: microservices/protocol-service/Dockerfile - image: radarbase/appserver-protocol-service:test + image: radarbase/appserver-protocol-service:SNAPSHOT networks: - microservice @@ -185,7 +185,7 @@ services: build: context: .. dockerfile: microservices/task-service/Dockerfile - image: radarbase/appserver-task-service:test + image: radarbase/appserver-task-service:SNAPSHOT networks: - microservice - task-service-internal @@ -214,7 +214,7 @@ services: build: context: .. dockerfile: microservices/cloud-messaging-service/Dockerfile - image: radarbase/appserver-cloud-messaging-service:test + image: radarbase/appserver-cloud-messaging-service:SNAPSHOT networks: - microservice - cloud-messaging-service-internal @@ -226,4 +226,8 @@ services: APPSERVER_CLOUD_MESSAGING_JDBC_PASSWORD: radar APPSERVER_CLOUD_MESSAGING_HIBERNATE_DIALECT: org.hibernate.dialect.PostgreSQLDialect APPSERVER_CLOUD_MESSAGING_JDBC_DRIVER: org.postgresql.Driver + GOOGLE_APPLICATION_CREDENTIALS: /appserver-includes/google-credentials.json + volumes: + - ./integration-tests/src/integrationTest/resources/docker/fcm:/appserver-includes/ + diff --git a/microservices/gateway-service/Dockerfile b/microservices/gateway-service/Dockerfile index ad44fd845..03c5d9b51 100644 --- a/microservices/gateway-service/Dockerfile +++ b/microservices/gateway-service/Dockerfile @@ -5,7 +5,6 @@ WORKDIR /code ENV GRADLE_USER_HOME=/code/.gradlecache \ GRADLE_OPTS="-Djdk.lang.Process.launchMechanism=vfork -Dorg.gradle.vfs.watch=false" -# "-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs=-Xmx1g -Dorg.gradle.workers.max=2" COPY buildSrc /code/buildSrc COPY build.gradle.kts settings.gradle.kts /code/ diff --git a/microservices/gateway-service/build.gradle.kts b/microservices/gateway-service/build.gradle.kts index abb8a1db3..646549d53 100644 --- a/microservices/gateway-service/build.gradle.kts +++ b/microservices/gateway-service/build.gradle.kts @@ -15,3 +15,8 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") } + +ktlint { + ignoreFailures.set(true) + outputColorName.set("RED") +} diff --git a/microservices/gateway-service/src/main/kotlin/org/radarbase/appserver/microservices/gateway/api/GatewayResource.kt b/microservices/gateway-service/src/main/kotlin/org/radarbase/appserver/microservices/gateway/api/GatewayResource.kt index c06eeba1b..1326d3c57 100644 --- a/microservices/gateway-service/src/main/kotlin/org/radarbase/appserver/microservices/gateway/api/GatewayResource.kt +++ b/microservices/gateway-service/src/main/kotlin/org/radarbase/appserver/microservices/gateway/api/GatewayResource.kt @@ -264,6 +264,14 @@ class GatewayResource @Inject constructor( @Suspended asyncResponse: AsyncResponse, ) { asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + tokenForCurrentRequest(asyncService, tokenProvider).also { + authService.checkPermission( + Permission.SUBJECT_READ, + EntityDetails(project = projectId, subject = it.subject), + it, + ) + } + val proxyResponse = ProjectServiceContract.getProjectUsingProjectId( projectId, projectServiceRoute.baseUrl, @@ -287,15 +295,6 @@ class GatewayResource @Inject constructor( } if (decodedProject == null) throw InvalidUpstreamResponseException() - - val projectIdForAuth = decodedProject.projectId ?: projectId - val token = tokenForCurrentRequest(asyncService, tokenProvider) - authService.checkPermission( - Permission.SUBJECT_READ, - EntityDetails(project = projectIdForAuth, subject = token.subject), - token, - ) - Response.ok(decodedProject).build() } } @@ -578,6 +577,13 @@ class GatewayResource @Inject constructor( @Suspended asyncResponse: AsyncResponse, ) { asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + val token = tokenForCurrentRequest(asyncService, tokenProvider) + authService.checkPermission( + Permission.SUBJECT_READ, + EntityDetails(project = projectId, subject = token.subject), + token, + ) + val users = UserServiceContract.getUsersUsingProjectId(projectId, userServiceRoute.baseUrl).let { if (it.status !in 200..299) { return@runAsCoroutine handleProxyResponse(it) @@ -587,12 +593,6 @@ class GatewayResource @Inject constructor( (dtoFromProxyResponse(json, it) ?: throw InvalidUpstreamResponseException()) } } - val token = tokenForCurrentRequest(asyncService, tokenProvider) - authService.checkPermission( - Permission.SUBJECT_READ, - EntityDetails(project = projectId, subject = token.subject), - token, - ) Response.ok(users).build() } } @@ -885,7 +885,7 @@ class GatewayResource @Inject constructor( @Path("${PROJECTS_PATH}/${PROJECT_ID}/$MESSAGING_NOTIFICATION_PATH") @Produces(APPLICATION_JSON) @Authenticated - @NeedsPermission(Permission.SUBJECT_READ, projectPathParam = "projectId") + @NeedsPermission(Permission.SUBJECT_READ) fun getNotificationsUsingProjectId( @Valid @PathParam("projectId") projectId: String, @Suspended asyncResponse: AsyncResponse, diff --git a/microservices/gateway-service/src/main/resources/gateway-service.yml b/microservices/gateway-service/src/main/resources/gateway-service.yml index e3dd366b3..ab4bf3c76 100644 --- a/microservices/gateway-service/src/main/resources/gateway-service.yml +++ b/microservices/gateway-service/src/main/resources/gateway-service.yml @@ -4,7 +4,7 @@ externalPrefix: auth: managementPortalUrl: http://localhost:8081/managementportal - resourceName: res_appconfig + resourceName: res_AppServer routes: - name: project diff --git a/microservices/github-service/build.gradle.kts b/microservices/github-service/build.gradle.kts index cc6b1a068..35204af1e 100644 --- a/microservices/github-service/build.gradle.kts +++ b/microservices/github-service/build.gradle.kts @@ -19,3 +19,8 @@ dependencies { implementation("io.ktor:ktor-client-content-negotiation:${Versions.ktorVersion}") implementation("io.ktor:ktor-serialization-kotlinx-json:${Versions.ktorVersion}") } + +ktlint { + ignoreFailures.set(true) + outputColorName.set("RED") +} diff --git a/microservices/integration-tests/build.gradle.kts b/microservices/integration-tests/build.gradle.kts new file mode 100644 index 000000000..258363f19 --- /dev/null +++ b/microservices/integration-tests/build.gradle.kts @@ -0,0 +1,64 @@ +import java.time.Duration + +plugins { + kotlin("plugin.serialization") version Versions.kotlinVersion + id("com.avast.gradle.docker-compose") version Versions.dockerCompose +} + +description = "Integration tests for radar appserver microservices." + +val integrationTestSourceSet = sourceSets.create("integrationTest") { + compileClasspath += sourceSets.main.get().output + runtimeClasspath += sourceSets.main.get().output +} + +val integrationTestImplementation: Configuration by configurations.getting { + extendsFrom(configurations.testImplementation.get()) +} + +val integrationTest by tasks.registering(Test::class) { + description = "Runs integration tests." + group = "verification" + testClassesDirs = integrationTestSourceSet.output.classesDirs + classpath = integrationTestSourceSet.runtimeClasspath + testLogging.showStandardStreams = true + shouldRunAfter("test") + outputs.upToDateWhen { false } +} + +dockerCompose { + useComposeFiles.set(listOf("src/integrationTest/resources/docker/docker-compose.yml")) + val dockerComposeBuild: String? by project + val doBuild = dockerComposeBuild?.toBoolean() ?: true + buildBeforeUp.set(doBuild) + buildBeforePull.set(doBuild) + buildAdditionalArgs.set(emptyList()) + val dockerComposeStopContainers: String? by project + stopContainers.set(dockerComposeStopContainers?.toBoolean() ?: true) + waitForTcpPortsTimeout.set(Duration.ofMinutes(3)) + environment.put("SERVICES_HOST", "localhost") + captureContainersOutputToFiles.set(project.file("build/container-logs")) + isRequiredBy(integrationTest) +} + +configurations["integrationTestRuntimeOnly"].extendsFrom(configurations.testRuntimeOnly.get()) + +dependencies { + integrationTestImplementation(project(":microservices:core")) + integrationTestImplementation("org.radarbase:radar-jersey:${Versions.radarJerseyVersion}") + integrationTestImplementation("org.radarbase:radar-commons-kotlin:${Versions.radarCommonsVersion}") + integrationTestImplementation("io.mockk:mockk:1.14.4") + integrationTestImplementation("org.mockito.kotlin:mockito-kotlin:3.2.0") + integrationTestImplementation("org.hamcrest:hamcrest:2.1") + integrationTestImplementation("org.assertj:assertj-core:3.24.2") + integrationTestImplementation(platform("io.ktor:ktor-bom:${Versions.ktorVersion}")) + integrationTestImplementation("io.ktor:ktor-client-core:${Versions.ktorVersion}") + integrationTestImplementation("io.ktor:ktor-client-cio:${Versions.ktorVersion}") + integrationTestImplementation("io.ktor:ktor-client-content-negotiation") + integrationTestImplementation("io.ktor:ktor-serialization-kotlinx-json") +} + +ktlint { + ignoreFailures.set(true) + outputColorName.set("RED") +} diff --git a/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/NotificationEndpointAuthTest.kt b/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/NotificationEndpointAuthTest.kt new file mode 100644 index 000000000..387ac11f5 --- /dev/null +++ b/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/NotificationEndpointAuthTest.kt @@ -0,0 +1,234 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.microservices + +import io.ktor.client.HttpClient +import io.ktor.client.request.accept +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.http.headersOf +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.radarbase.appserver.microservices.commons.MpOAuthSupport +import org.radarbase.appserver.microservices.core.dto.ProjectDto +import org.radarbase.appserver.microservices.core.dto.fcm.FcmNotificationDto +import org.radarbase.appserver.microservices.core.dto.fcm.FcmNotifications +import org.radarbase.appserver.microservices.core.dto.fcm.FcmUserDto +import java.time.Duration +import java.time.Instant + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class NotificationEndpointAuthTest { + + val notification = FcmNotificationDto().apply { + scheduledTime = Instant.now().plus(Duration.ofSeconds(100)) + body = "Test Body" + sourceId = "test-source" + title = "Test Title" + ttlSeconds = 86400 + fcmMessageId = "123455" + additionalData = mutableMapOf() + appPackage = "armt" + sourceType = "armt" + type = "ESM" + } + + @BeforeEach + fun createUserAndProject(): Unit = runBlocking { + val project = ProjectDto(projectId = "radar") + + httpClient.post(PROJECT_PATH) { + contentType(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + setBody(project) + } + + val fcmUserDto = FcmUserDto( + projectId = "radar", + language = "en", + enrolmentDate = Instant.now(), + fcmToken = "xxx", + subjectId = "sub-1", + timezone = "Europe/London", + ) + + httpClient.post("$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH") { + contentType(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + setBody(fcmUserDto) + } + } + + @Test + fun getUnAuthorizedNotificationsForUser(): Unit = runBlocking { + val response = httpClient.get( + "$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/$DEFAULT_USER/$NOTIFICATION_PATH", + ) { + accept(ContentType.Application.Json) + } + + assertEquals(HttpStatusCode.Unauthorized, response.status) + } + + @Test + fun getUnAuthorizedNotificationsForProject(): Unit = runBlocking { + val response = httpClient.get( + "$PROJECT_PATH/$DEFAULT_PROJECT/$NOTIFICATION_PATH", + ) { + accept(ContentType.Application.Json) + } + + assertEquals(HttpStatusCode.Unauthorized, response.status) + } + + @Test + fun postUnauthorizedNotificationForUser(): Unit = runBlocking { + val response = httpClient.post( + "$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/$DEFAULT_USER/$NOTIFICATION_PATH", + ) { + contentType(ContentType.Application.Json) + setBody(notification) + } + + assertEquals(HttpStatusCode.Unauthorized, response.status) + } + + @Order(1) + @Test + fun postNotificationForUser(): Unit = runBlocking { + val response = httpClient.post( + "$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/$DEFAULT_USER/$NOTIFICATION_PATH", + ) { + setBody(notification) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + contentType(ContentType.Application.Json) + } + + assertEquals(HttpStatusCode.Created, response.status) + } + + @Order(2) + @Test + fun postBatchNotificationForUser(): Unit = runBlocking { + val singleNotification = notification + val notifications = FcmNotifications( + mutableListOf( + singleNotification.apply { + title = "Test Title 1" + fcmMessageId = "xxxyyyy" + }, + ), + ) + + val response = httpClient.post( + "$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/$DEFAULT_USER/$NOTIFICATION_PATH/batch", + ) { + setBody(notifications) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + contentType(ContentType.Application.Json) + } + + assertEquals(HttpStatusCode.OK, response.status) + } + + @Test + fun getNotificationsForUser(): Unit = runBlocking { + val response = httpClient.get( + "$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/$DEFAULT_USER/$NOTIFICATION_PATH", + ) { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(HttpStatusCode.OK, response.status) + } + + @Test + fun getNotificationsForProject(): Unit = runBlocking { + val response = httpClient.get( + "$PROJECT_PATH/$DEFAULT_PROJECT/$NOTIFICATION_PATH", + ) { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(HttpStatusCode.OK, response.status) + } + + @Test + fun viewForbiddenNotificationsForOtherUser(): Unit = runBlocking { + val response = httpClient.get( + "$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/sub-2/$NOTIFICATION_PATH", + ) { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(HttpStatusCode.Forbidden, response.status) + } + + @Test + fun viewForbiddenNotificationsForOtherProject(): Unit = runBlocking { + val response = httpClient.get( + "$PROJECT_PATH/other-project/$NOTIFICATION_PATH", + ) { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(HttpStatusCode.Forbidden, response.status) + } + + companion object { + private lateinit var AUTH_HEADERS: Headers + private lateinit var httpClient: HttpClient + private const val PROJECT_PATH = "projects" + private const val USER_PATH = "users" + private const val DEFAULT_PROJECT = "radar" + private const val DEFAULT_USER = "sub-1" + private const val NOTIFICATION_PATH = "messaging/notifications" + + @BeforeAll + @JvmStatic + fun init() { + httpClient = MpOAuthSupport.initHttpClient() + + val oAuthSupport = MpOAuthSupport().apply { + init() + } + AUTH_HEADERS = runBlocking { + headersOf( + HttpHeaders.Authorization, + "Bearer ${oAuthSupport.requestAccessToken()}", + ) + } + } + } +} diff --git a/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/ProjectEndpointAuthTest.kt b/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/ProjectEndpointAuthTest.kt new file mode 100644 index 000000000..e1740289c --- /dev/null +++ b/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/ProjectEndpointAuthTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.microservices + +import io.ktor.client.HttpClient +import io.ktor.client.request.accept +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.http.headersOf +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.radarbase.appserver.microservices.commons.MpOAuthSupport +import org.radarbase.appserver.microservices.core.dto.ProjectDto + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class ProjectEndpointAuthTest { + + @Test + fun unAuthorizedCreatedProject(): Unit = runBlocking { + val project = ProjectDto(projectId = "radar") + val response = httpClient.post(PROJECT_PATH) { + contentType(ContentType.Application.Json) + setBody(project) + } + assertEquals(HttpStatusCode.Unauthorized, response.status) + } + + @Test + fun unAuthorizedViewProjects(): Unit = runBlocking { + val response = httpClient.get(PROJECT_PATH) { + accept(ContentType.Application.Json) + } + assertEquals(HttpStatusCode.Unauthorized, response.status) + } + + @Test + fun unAuthorizedViewSingleProject(): Unit = runBlocking { + val response = httpClient.get("$PROJECT_PATH/radar") { + accept(ContentType.Application.Json) + } + assertEquals(HttpStatusCode.Unauthorized, response.status) + } + + @Test + fun forbiddenViewProjects(): Unit = runBlocking { + val response = httpClient.get(PROJECT_PATH) { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + // Only Admins Can View List Of All Projects + assertEquals(HttpStatusCode.Forbidden, response.status) + } + + @Test + @Order(1) + fun createSingleProjectWithAuth() = runBlocking { + val project = ProjectDto(projectId = "radar") + + val response = httpClient.post(PROJECT_PATH) { + contentType(ContentType.Application.Json) + setBody(project) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + if (response.status == HttpStatusCode.ExpectationFailed) { + return@runBlocking + } + assertEquals(HttpStatusCode.Created, response.status) + } + + @Test + @Order(2) + fun getSingleProjectWithAuth() = runBlocking { + + val response = httpClient.get("$PROJECT_PATH/radar") { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(HttpStatusCode.OK, response.status) + } + + @Test + @Order(3) + fun getForbiddenProjectWithAuth() = runBlocking { + val response = httpClient.get("$PROJECT_PATH/test") { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + assertEquals(HttpStatusCode.Forbidden, response.status) + } + + companion object { + private const val PROJECT_PATH = "projects" + private lateinit var AUTH_HEADERS: Headers + private lateinit var httpClient: HttpClient + + @BeforeAll + @JvmStatic + fun init() { + httpClient = MpOAuthSupport.initHttpClient() + + val oAuthSupport = MpOAuthSupport().also { + it.init() + } + + AUTH_HEADERS = runBlocking { + headersOf( + HttpHeaders.Authorization, + "Bearer ${oAuthSupport.requestAccessToken()}", + ) + } + } + } +} diff --git a/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/UserEndpointAuthTest.kt b/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/UserEndpointAuthTest.kt new file mode 100644 index 000000000..61cef39a7 --- /dev/null +++ b/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/UserEndpointAuthTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.microservices + +import io.ktor.client.HttpClient +import io.ktor.client.request.accept +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.http.headersOf +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.radarbase.appserver.microservices.commons.MpOAuthSupport +import org.radarbase.appserver.microservices.core.dto.ProjectDto +import org.radarbase.appserver.microservices.core.dto.fcm.FcmUserDto +import java.time.Instant + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class UserEndpointAuthTest { + val fcmUserDto = FcmUserDto( + projectId = DEFAULT_PROJECT, + language = "en", + enrolmentDate = Instant.now(), + fcmToken = "xxx", + subjectId = "sub-1", + timezone = "Europe/London", + ) + + @BeforeEach + fun postDefaultProject(): Unit = runBlocking { + val project = ProjectDto(projectId = DEFAULT_PROJECT) + + httpClient.post(PROJECT_PATH) { + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + contentType(ContentType.Application.Json) + setBody(project) + } + } + + @Test + fun viewUnauthorizedSingleUser(): Unit = runBlocking { + val response = httpClient.get("$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/sub-1") { + accept(ContentType.Application.Json) + } + + assertEquals(HttpStatusCode.Unauthorized, response.status) + } + + @Test + fun createUnAuthorizedUser(): Unit = runBlocking { + val response = httpClient.post("$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH") { + contentType(ContentType.Application.Json) + setBody(fcmUserDto) + } + + assertEquals(HttpStatusCode.Unauthorized, response.status) + } + + @Test + @Order(1) + fun postUser(): Unit = runBlocking { + val response = httpClient.post("$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH") { + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + contentType(ContentType.Application.Json) + setBody(fcmUserDto) + } + if (response.status == HttpStatusCode.ExpectationFailed) { + return@runBlocking + } + + assertEquals(HttpStatusCode.Created, response.status) + } + + @Test + @Order(2) + fun getUser(): Unit = runBlocking { + val response = httpClient.get("$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/sub-1") { + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + accept(ContentType.Application.Json) + } + + assertEquals(response.status, HttpStatusCode.OK) + } + + @Test + @Order(3) + fun getUsersInProject(): Unit = runBlocking { + val response = httpClient.get("$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH") { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(HttpStatusCode.OK, response.status) + } + + @Test + @Order(4) + fun viewForbiddenUsersInOtherProject(): Unit = runBlocking { + val response = httpClient.get("$PROJECT_PATH/other-project/$USER_PATH") { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(HttpStatusCode.Forbidden, response.status) + } + + @Test + @Order(5) + fun getAllUsers() = runBlocking { + val response = httpClient.get(USER_PATH) { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + // Should return a filtered list of users for which the token has access. + assertEquals(HttpStatusCode.OK, response.status) + } + + companion object { + private const val PROJECT_PATH = "projects" + private const val USER_PATH = "users" + private const val DEFAULT_PROJECT = "radar" + private lateinit var AUTH_HEADERS: Headers + private lateinit var httpClient: HttpClient + + @BeforeAll + @JvmStatic + fun init() { + httpClient = MpOAuthSupport.initHttpClient() + val oAuthSupport = MpOAuthSupport().apply { + init() + } + + AUTH_HEADERS = runBlocking { + headersOf( + HttpHeaders.Authorization, + "Bearer ${oAuthSupport.requestAccessToken()}", + ) + } + } + } +} diff --git a/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/commons/MPMetaToken.kt b/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/commons/MPMetaToken.kt new file mode 100644 index 000000000..bc3ef8564 --- /dev/null +++ b/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/commons/MPMetaToken.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.microservices.commons + +import kotlinx.serialization.Serializable + +@Serializable +data class MPMetaToken( + val refreshToken: String, +) diff --git a/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/commons/MPPairResponse.kt b/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/commons/MPPairResponse.kt new file mode 100644 index 000000000..78adcbee2 --- /dev/null +++ b/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/commons/MPPairResponse.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.microservices.commons + +import kotlinx.serialization.Serializable + +@Serializable +class MPPairResponse( + val tokenUrl: String, +) diff --git a/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/commons/MpOAuthSupport.kt b/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/commons/MpOAuthSupport.kt new file mode 100644 index 000000000..809540765 --- /dev/null +++ b/microservices/integration-tests/src/integrationTest/kotlin/org/radarbase/appserver/microservices/commons/MpOAuthSupport.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.microservices.commons + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.request.basicAuth +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.get +import io.ktor.http.HttpStatusCode +import io.ktor.http.Parameters +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.core.IsEqual.equalTo +import org.radarbase.ktor.auth.OAuth2AccessToken +import org.radarbase.ktor.auth.bearer + +class MpOAuthSupport { + private lateinit var httpClient: HttpClient + + fun init() { + httpClient = HttpClient(CIO) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + coerceInputValues = true + }, + ) + } + defaultRequest { + url("${MANAGEMENTPORTAL_URL}/") + } + } + } + + suspend fun requestAccessToken(): String { + val response = httpClient.submitForm( + url = "oauth/token", + formParameters = Parameters.build { + append("username", ADMIN_USER) + append("password", ADMIN_PASSWORD) + append("grant_type", "password") + }, + ) { + basicAuth(username = MP_CLIENT, password = "") + } + assertThat(response.status, equalTo(HttpStatusCode.OK)) + val token = response.body() + + val tokenUrl = httpClient.get("api/oauth-clients/pair") { + url { + parameters.append("clientId", REST_CLIENT) + parameters.append("login", "sub-1") + parameters.append("persistent", "false") + } + bearer(requireNotNull(token.accessToken)) + }.body().tokenUrl + + println("Requesting refresh token") + val refreshToken = httpClient.get(tokenUrl).body().refreshToken + + return requireNotNull( + httpClient.submitForm( + url = "oauth/token", + formParameters = Parameters.build { + append("grant_type", "refresh_token") + append("refresh_token", refreshToken) + }, + ) { + basicAuth(REST_CLIENT, "") + }.body().accessToken, + ) + } + + companion object { + private const val APPSERVER_URL = "http://localhost:8080" + private const val MANAGEMENTPORTAL_URL = "http://localhost:8081/managementportal" + const val MP_CLIENT = "ManagementPortalapp" + const val REST_CLIENT = "pRMT" + const val ADMIN_USER = "admin" + const val ADMIN_PASSWORD = "admin" + + fun initHttpClient(): HttpClient = HttpClient(CIO) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + coerceInputValues = true + }, + ) + } + defaultRequest { + url("${APPSERVER_URL}/") + } + } + } +} diff --git a/microservices/integration-tests/src/integrationTest/resources/docker/.env b/microservices/integration-tests/src/integrationTest/resources/docker/.env new file mode 100644 index 000000000..00e354e88 --- /dev/null +++ b/microservices/integration-tests/src/integrationTest/resources/docker/.env @@ -0,0 +1,14 @@ +RADAR_APPSERVER_GATEWAY_SERVICE_IMAGE_NAME=ghcr.io/radar-base/radar-appserver/gateway-service +RADAR_APPSERVER_PROJECT_SERVICE_IMAGE_NAME=ghcr.io/radar-base/radar-appserver/project-service +RADAR_APPSERVER_USER_SERVICE_IMAGE_NAME=ghcr.io/radar-base/radar-appserver/user-service +RADAR_APPSERVER_GITHUB_SERVICE_IMAGE_NAME=ghcr.io/radar-base/radar-appserver/github-service +RADAR_APPSERVER_PROTOCOL_SERVICE_IMAGE_NAME=ghcr.io/radar-base/radar-appserver/protocol-service +RADAR_APPSERVER_TASK_SERVICE_IMAGE_NAME=ghcr.io/radar-base/radar-appserver/task-service +RADAR_APPSERVER_CLOUD_MESSAGING_SERVICE_IMAGE_NAME=ghcr.io/radar-base/radar-appserver/cloud-messaging-service +RADAR_APPSERVER_GATEWAY_SERVICE_IMAGE_TAG=SNAPSHOT +RADAR_APPSERVER_PROJECT_SERVICE_IMAGE_TAG=SNAPSHOT +RADAR_APPSERVER_USER_SERVICE_IMAGE_TAG=SNAPSHOT +RADAR_APPSERVER_GITHUB_SERVICE_IMAGE_TAG=SNAPSHOT +RADAR_APPSERVER_PROTOCOL_SERVICE_IMAGE_TAG=SNAPSHOT +RADAR_APPSERVER_TASK_SERVICE_IMAGE_TAG=SNAPSHOT +RADAR_APPSERVER_CLOUD_MESSAGING_SERVICE_IMAGE_TAG=SNAPSHOT diff --git a/microservices/integration-tests/src/integrationTest/resources/docker/docker-compose.yml b/microservices/integration-tests/src/integrationTest/resources/docker/docker-compose.yml new file mode 100644 index 000000000..86d3b1606 --- /dev/null +++ b/microservices/integration-tests/src/integrationTest/resources/docker/docker-compose.yml @@ -0,0 +1,233 @@ +version: '3.8' + +#networks: +# public: +# driver: bridge +# mp: +# driver: bridge +# internal: true +# mp-db: +# driver: bridge +# internal: true +# microservice: +# driver: bridge +# internal: true +# project-service-internal: +# driver: bridge +# internal: true +# user-service-internal: +# driver: bridge +# internal: true +# task-service-internal: +# driver: bridge +# internal: true +# cloud-messaging-service-internal: +# driver: bridge +# internal: true + +services: + #---------------------------------------------------------------------------# + # ManagementPortal Postgres # + #---------------------------------------------------------------------------# + managementportal-postgresql: + image: postgres + environment: + POSTGRES_USER: radarbase + POSTGRES_PASSWORD: radarbase + POSTGRES_DB: managementportal +# networks: +# - mp-db + + #---------------------------------------------------------------------------# + # Management Portal # + #---------------------------------------------------------------------------# + managementportal: + image: radarbase/management-portal:2.1.0 + environment: + SERVER_PORT: 8081 + SPRING_PROFILES_ACTIVE: prod + SPRING_DATASOURCE_URL: jdbc:postgresql://managementportal-postgresql:5432/managementportal + SPRING_DATASOURCE_USERNAME: radarbase + SPRING_DATASOURCE_PASSWORD: radarbase + SPRING_LIQUIBASE_CONTEXTS: dev #includes testing_data, remove for production builds + JHIPSTER_SLEEP: 10 # gives time for the database to boot before the application + JAVA_OPTS: -Xmx512m # maximum heap size for the JVM running ManagementPortal, increase this as necessary + MANAGEMENTPORTAL_COMMON_BASE_URL: http://localhost:8081/managementportal + MANAGEMENTPORTAL_COMMON_MANAGEMENT_PORTAL_BASE_URL: http://localhost:8081/managementportal + MANAGEMENTPORTAL_FRONTEND_CLIENT_SECRET: + MANAGEMENTPORTAL_OAUTH_CLIENTS_FILE: /mp-includes/config/oauth_client_details.csv +# networks: +# - public +# - mp +# - mp-db + ports: + - "8081:8081" + volumes: + - ./etc/:/mp-includes/ + + #---------------------------------------------------------------------------# + # Gateway Service # + #---------------------------------------------------------------------------# + gateway-service: + build: + context: ../../../../../../ + dockerfile: microservices/gateway-service/Dockerfile + image: ${RADAR_APPSERVER_GATEWAY_SERVICE_IMAGE_NAME}:${RADAR_APPSERVER_GATEWAY_SERVICE_IMAGE_TAG} + ports: + - "8080:8080" +# networks: +# - public +# - microservice +# - mp + environment: + APPSERVER_PROJECT_SERVICE_BASE_URL: http://project-service:9010 + APPSERVER_USER_SERVICE_BASE_URL: http://user-service:9013 + APPSERVER_GITHUB_SERVICE_BASE_URL: http://github-service:9011 + APPSERVER_PROTOCOL_SERVICE_BASE_URL: http://protocol-service:9012 + APPSERVER_TASK_SERVICE_BASE_URL: http://task-service:9014 + APPSERVER_CLOUD_MESSAGING_SERVICE_BASE_URL: http://cloud-messaging-service:9015 + APPSERVER_MANAGEMENTPORTAL_BASE_URL: http://managementportal:8081/managementportal + + #---------------------------------------------------------------------------# + # Project Service # + #---------------------------------------------------------------------------# + project-service-db: + image: postgres +# networks: +# - project-service-internal + environment: + POSTGRES_DB: appserver_project + POSTGRES_USER: radar + POSTGRES_PASSWORD: radar + + project-service: + build: + context: ../../../../../../ + dockerfile: microservices/project-service/Dockerfile + image: ${RADAR_APPSERVER_PROJECT_SERVICE_IMAGE_NAME}:${RADAR_APPSERVER_PROJECT_SERVICE_IMAGE_TAG} +# networks: +# - microservice +# - project-service-internal + depends_on: + - project-service-db + environment: + APPSERVER_PROJECT_JDBC_URL: jdbc:postgresql://project-service-db:5432/appserver_project + APPSERVER_PROJECT_JDBC_USERNAME: radar + APPSERVER_PROJECT_JDBC_PASSWORD: radar + APPSERVER_PROJECT_HIBERNATE_DIALECT: org.hibernate.dialect.PostgreSQLDialect + APPSERVER_PROJECT_JDBC_DRIVER: org.postgresql.Driver + + #---------------------------------------------------------------------------# + # User Service # + #---------------------------------------------------------------------------# + user-service-db: + image: postgres +# networks: +# - user-service-internal + environment: + POSTGRES_DB: appserver_user + POSTGRES_USER: radar + POSTGRES_PASSWORD: radar + + user-service: + build: + context: ../../../../../../ + dockerfile: microservices/user-service/Dockerfile + image: ${RADAR_APPSERVER_USER_SERVICE_IMAGE_NAME}:${RADAR_APPSERVER_USER_SERVICE_IMAGE_TAG} +# networks: +# - microservice +# - user-service-internal + depends_on: + - user-service-db + environment: + APPSERVER_USER_JDBC_URL: jdbc:postgresql://user-service-db:5432/appserver_user + APPSERVER_USER_JDBC_USERNAME: radar + APPSERVER_USER_JDBC_PASSWORD: radar + APPSERVER_USER_HIBERNATE_DIALECT: org.hibernate.dialect.PostgreSQLDialect + APPSERVER_USER_JDBC_DRIVER: org.postgresql.Driver + + #---------------------------------------------------------------------------# + # Github Service # + #---------------------------------------------------------------------------# + github-service: + build: + context: ../../../../../../ + dockerfile: microservices/github-service/Dockerfile + image: ${RADAR_APPSERVER_GITHUB_SERVICE_IMAGE_NAME}:${RADAR_APPSERVER_GITHUB_SERVICE_IMAGE_TAG} +# networks: +# - microservice +# - public + + #---------------------------------------------------------------------------# + # Protocol Service # + #---------------------------------------------------------------------------# + protocol-service: + build: + context: ../../../../../../ + dockerfile: microservices/protocol-service/Dockerfile + image: ${RADAR_APPSERVER_PROTOCOL_SERVICE_IMAGE_NAME}:${RADAR_APPSERVER_PROTOCOL_SERVICE_IMAGE_TAG} +# networks: +# - microservice + + #---------------------------------------------------------------------------# + # Task Service # + #---------------------------------------------------------------------------# + task-service-db: + image: postgres +# networks: +# - task-service-internal + environment: + POSTGRES_DB: appserver_task + POSTGRES_USER: radar + POSTGRES_PASSWORD: radar + + task-service: + build: + context: ../../../../../../ + dockerfile: microservices/task-service/Dockerfile + image: ${RADAR_APPSERVER_TASK_SERVICE_IMAGE_NAME}:${RADAR_APPSERVER_TASK_SERVICE_IMAGE_TAG} +# networks: +# - microservice +# - task-service-internal + depends_on: + - task-service-db + environment: + APPSERVER_TASK_JDBC_URL: jdbc:postgresql://task-service-db:5432/appserver_task + APPSERVER_TASK_JDBC_USERNAME: radar + APPSERVER_TASK_JDBC_PASSWORD: radar + APPSERVER_TASK_HIBERNATE_DIALECT: org.hibernate.dialect.PostgreSQLDialect + APPSERVER_TASK_JDBC_DRIVER: org.postgresql.Driver + + #---------------------------------------------------------------------------# + # Cloud Messaging Service # + #---------------------------------------------------------------------------# + cloud-messaging-service-db: + image: postgres +# networks: +# - cloud-messaging-service-internal + environment: + POSTGRES_DB: appserver_cloud_messaging + POSTGRES_USER: radar + POSTGRES_PASSWORD: radar + + cloud-messaging-service: + build: + context: ../../../../../../ + dockerfile: microservices/cloud-messaging-service/Dockerfile + image: ${RADAR_APPSERVER_CLOUD_MESSAGING_SERVICE_IMAGE_NAME}:${RADAR_APPSERVER_CLOUD_MESSAGING_SERVICE_IMAGE_TAG} +# networks: +# - microservice +# - cloud-messaging-service-internal + depends_on: + - cloud-messaging-service-db + environment: + APPSERVER_CLOUD_MESSAGING_JDBC_URL: jdbc:postgresql://cloud-messaging-service-db:5432/appserver_cloud_messaging + APPSERVER_CLOUD_MESSAGING_JDBC_USERNAME: radar + APPSERVER_CLOUD_MESSAGING_JDBC_PASSWORD: radar + APPSERVER_CLOUD_MESSAGING_HIBERNATE_DIALECT: org.hibernate.dialect.PostgreSQLDialect + APPSERVER_CLOUD_MESSAGING_JDBC_DRIVER: org.postgresql.Driver + GOOGLE_APPLICATION_CREDENTIALS: /appserver-includes/google-credentials.json + volumes: + - ./fcm:/appserver-includes/ + + diff --git a/microservices/resources/docker/etc/config/keystore.p12 b/microservices/integration-tests/src/integrationTest/resources/docker/etc/config/keystore.p12 similarity index 100% rename from microservices/resources/docker/etc/config/keystore.p12 rename to microservices/integration-tests/src/integrationTest/resources/docker/etc/config/keystore.p12 diff --git a/microservices/resources/docker/etc/config/oauth_client_details.csv b/microservices/integration-tests/src/integrationTest/resources/docker/etc/config/oauth_client_details.csv similarity index 100% rename from microservices/resources/docker/etc/config/oauth_client_details.csv rename to microservices/integration-tests/src/integrationTest/resources/docker/etc/config/oauth_client_details.csv diff --git a/microservices/integration-tests/src/integrationTest/resources/docker/fcm/google-credentials.enc.gpg b/microservices/integration-tests/src/integrationTest/resources/docker/fcm/google-credentials.enc.gpg new file mode 100644 index 000000000..e83aaffb1 Binary files /dev/null and b/microservices/integration-tests/src/integrationTest/resources/docker/fcm/google-credentials.enc.gpg differ diff --git a/microservices/project-service/build.gradle.kts b/microservices/project-service/build.gradle.kts index 705db303d..26f38457f 100644 --- a/microservices/project-service/build.gradle.kts +++ b/microservices/project-service/build.gradle.kts @@ -17,3 +17,8 @@ dependencies { implementation(project(":microservices:core")) implementation(project(":microservices:contract")) } +ktlint { + ignoreFailures.set(true) + outputColorName.set("RED") +} + diff --git a/microservices/protocol-service/build.gradle.kts b/microservices/protocol-service/build.gradle.kts index 11f25a75e..9b4446f78 100644 --- a/microservices/protocol-service/build.gradle.kts +++ b/microservices/protocol-service/build.gradle.kts @@ -15,3 +15,8 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") } + +ktlint { + ignoreFailures.set(true) + outputColorName.set("RED") +} diff --git a/microservices/task-service/build.gradle.kts b/microservices/task-service/build.gradle.kts index 4fd6efd63..196c90fef 100644 --- a/microservices/task-service/build.gradle.kts +++ b/microservices/task-service/build.gradle.kts @@ -17,3 +17,8 @@ dependencies { implementation(project(":microservices:core")) implementation(project(":microservices:contract")) } + +ktlint { + ignoreFailures.set(true) + outputColorName.set("RED") +} diff --git a/microservices/task-service/src/main/kotlin/org/radarbase/appserver/microservices/task/service/questionnaire/schedule/QuestionnaireScheduleService.kt b/microservices/task-service/src/main/kotlin/org/radarbase/appserver/microservices/task/service/questionnaire/schedule/QuestionnaireScheduleService.kt index e05665c9d..6011fa428 100644 --- a/microservices/task-service/src/main/kotlin/org/radarbase/appserver/microservices/task/service/questionnaire/schedule/QuestionnaireScheduleService.kt +++ b/microservices/task-service/src/main/kotlin/org/radarbase/appserver/microservices/task/service/questionnaire/schedule/QuestionnaireScheduleService.kt @@ -19,6 +19,8 @@ package org.radarbase.appserver.microservices.task.service.questionnaire.schedul import jakarta.inject.Inject import jakarta.inject.Named import jakarta.ws.rs.core.Response +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.radarbase.appserver.microservices.contract.calls.NotificationServiceContract import org.radarbase.appserver.microservices.contract.calls.ProjectServiceContract import org.radarbase.appserver.microservices.contract.calls.ProtocolServiceContract @@ -73,6 +75,7 @@ class QuestionnaireScheduleService @Inject constructor( private val userServiceUrl = config.contract.user private val notificationServiceUrl = config.contract.notification + private val scheduleGeneratorMutex = Mutex() private val cleanScheduleRef: SchedulingService.RepeatReference = schedulingService.repeat( Duration.ofMillis(3_600_000), Duration.ofMillis(5_000), @@ -128,37 +131,39 @@ class QuestionnaireScheduleService @Inject constructor( } suspend fun generateScheduleForUser(user: User): Schedule { - val subjectId: String? = user.subjectId - checkNotNull(subjectId) { "Subject ID cannot be null in questionnaire scheduler service." } - val protocol: Protocol? = try { - ProtocolServiceContract.getProtocolForSubject( - requireNotNullField(user.projectId, "User's projectId"), - subjectId, - protocolServiceUrl, - ).let { - deserializeDtoFromContract(it) { - "protocol_not_found ; No protocol found for user $subjectId and project ${user.projectId}" + scheduleGeneratorMutex.withLock { + val subjectId: String? = user.subjectId + checkNotNull(subjectId) { "Subject ID cannot be null in questionnaire scheduler service." } + val protocol: Protocol? = try { + ProtocolServiceContract.getProtocolForSubject( + requireNotNullField(user.projectId, "User's projectId"), + subjectId, + protocolServiceUrl, + ).let { + deserializeDtoFromContract(it) { + "protocol_not_found ; No protocol found for user $subjectId and project ${user.projectId}" + } } + } catch (ex: Exception) { + null } - } catch (ex: Exception) { - null - } - val newSchedule: Schedule = protocol?.let { - val prevSchedule: Schedule = getScheduleForSubject(subjectId) - val prevTimeZone: String = prevSchedule.timezone ?: checkNotNull(user.timezone) { - "User timezone cannot be null in questionnaire scheduler service." - } + val newSchedule: Schedule = protocol?.let { + val prevSchedule: Schedule = getScheduleForSubject(subjectId) + val prevTimeZone: String = prevSchedule.timezone ?: checkNotNull(user.timezone) { + "User timezone cannot be null in questionnaire scheduler service." + } - if ((prevSchedule.version != it.version) || (prevTimeZone != user.timezone)) { - removeScheduleForUser(user) - } - scheduleGeneratorService.generateScheduleForUser(user, it, prevSchedule) - } ?: Schedule() + if ((prevSchedule.version != it.version) || (prevTimeZone != user.timezone)) { + removeScheduleForUser(user) + } + scheduleGeneratorService.generateScheduleForUser(user, it, prevSchedule) + } ?: Schedule() - return newSchedule.also { - subjectScheduleMap[subjectId] = it - saveTasksAndNotifications(user, newSchedule.assessmentSchedules) + return newSchedule.also { + subjectScheduleMap[subjectId] = it + saveTasksAndNotifications(user, newSchedule.assessmentSchedules) + } } } @@ -229,7 +234,7 @@ class QuestionnaireScheduleService @Inject constructor( protocolServiceUrl, ).let { deserializeDtoFromContract(it) { - "protocol_not_found ; No protocol found for user $subjectId and project ${user.projectId}" + "protocol_not_found ; No protocol found for user $subjectId, and project ${user.projectId}" } } } catch (ex: Exception) { diff --git a/microservices/user-service/build.gradle.kts b/microservices/user-service/build.gradle.kts index c4b68e104..fe9a0c934 100644 --- a/microservices/user-service/build.gradle.kts +++ b/microservices/user-service/build.gradle.kts @@ -17,3 +17,8 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") } + +ktlint { + ignoreFailures.set(true) + outputColorName.set("RED") +}