From 49174a4ea9f1b6f6f50b026d0b8345dade068ebd Mon Sep 17 00:00:00 2001 From: liuhy Date: Tue, 31 Mar 2026 19:18:31 +0800 Subject: [PATCH 1/3] goalx: snapshot before shenyu-analysis --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ed771e556c44..e266500fd74d 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ Thumbs.db # Private individual user cursor rules .cursor/rules/_*.mdc + +# local worktrees +.worktrees/ From 0769cc83031dd3359565fad006aecb8a0d7c3215 Mon Sep 17 00:00:00 2001 From: liuhy Date: Wed, 15 Apr 2026 13:06:55 +0800 Subject: [PATCH 2/3] chore(ci): optimize workflow build cache and mvnd parallelism --- .../workflows/integrated-test-k8s-ingress.yml | 80 ++++--------------- .github/workflows/integrated-test.yml | 64 ++++----------- .github/workflows/k8s-examples-http.yml | 51 ++++-------- 3 files changed, 49 insertions(+), 146 deletions(-) diff --git a/.github/workflows/integrated-test-k8s-ingress.yml b/.github/workflows/integrated-test-k8s-ingress.yml index c6aa33c88b55..02b0c8dc0172 100644 --- a/.github/workflows/integrated-test-k8s-ingress.yml +++ b/.github/workflows/integrated-test-k8s-ingress.yml @@ -68,20 +68,14 @@ jobs: sudo rm -rf "/usr/local/share/boost" sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - name: Cache Maven Repos - if: steps.filter.outputs.k8s-ingress == 'true' - uses: actions/cache@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - uses: actions/setup-java@v4 if: steps.filter.outputs.k8s-ingress == 'true' with: java-version: 17 distribution: "temurin" + cache: "maven" + cache-dependency-path: | + **/pom.xml - name: Install Go uses: actions/setup-go@v3 @@ -95,41 +89,17 @@ jobs: curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.24.14/bin/linux/amd64/kubectl && sudo install kubectl /usr/local/bin/kubectl kind create cluster --image=kindest/node:v1.21.1 --config=./shenyu-integrated-test/${{ matrix.case }}/deploy/kind-config.yaml - - name: Cache Maven Repos - if: steps.filter.outputs.k8s-ingress == 'true' - uses: actions/cache@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - - uses: actions/setup-java@v4 - if: steps.filter.outputs.k8s-ingress == 'true' - with: - java-version: 17 - distribution: "temurin" - - name: Install mvnd if: steps.filter.outputs.k8s-ingress == 'true' shell: bash run: | MVND_VERSION=1.0.2 - if [[ "${{ runner.os }}" == "Windows" ]]; then - curl -sL https://downloads.apache.org/maven/mvnd/${MVND_VERSION}/maven-mvnd-${MVND_VERSION}-windows-amd64.zip -o mvnd.zip - unzip -q mvnd.zip - mkdir -p $HOME/.local - mv maven-mvnd-${MVND_VERSION}-windows-amd64 $HOME/.local/mvnd - echo "$HOME/.local/mvnd/bin" >> $GITHUB_PATH - echo "MVND_HOME=$HOME/.local/mvnd" >> $GITHUB_ENV - else - curl -sL https://downloads.apache.org/maven/mvnd/${MVND_VERSION}/maven-mvnd-${MVND_VERSION}-linux-amd64.zip -o mvnd.zip - unzip -q mvnd.zip - mkdir -p $HOME/.local - mv maven-mvnd-${MVND_VERSION}-linux-amd64 $HOME/.local/mvnd - echo "$HOME/.local/mvnd/bin" >> $GITHUB_PATH - echo "MVND_HOME=$HOME/.local/mvnd" >> $GITHUB_ENV - fi + curl -sL https://downloads.apache.org/maven/mvnd/${MVND_VERSION}/maven-mvnd-${MVND_VERSION}-linux-amd64.zip -o mvnd.zip + unzip -q mvnd.zip + mkdir -p $HOME/.local + mv maven-mvnd-${MVND_VERSION}-linux-amd64 $HOME/.local/mvnd + echo "$HOME/.local/mvnd/bin" >> $GITHUB_PATH + echo "MVND_HOME=$HOME/.local/mvnd" >> $GITHUB_ENV - name: Build with Maven if: steps.filter.outputs.k8s-ingress == 'true' @@ -137,14 +107,10 @@ jobs: run: | if mvnd --version > /dev/null 2>&1; then echo "Using mvnd for build" - mvnd -B clean install -Dmaven.javadoc.skip=true -Dmaven.test.skip=true + mvnd -B -T 1C clean install -Dmaven.javadoc.skip=true -Dmaven.test.skip=true else echo "Falling back to maven wrapper" - if [[ "${{ runner.os }}" == "Windows" ]]; then - ./mvnw.cmd -B clean install -Dmaven.javadoc.skip=true -Dmaven.test.skip=true - else - ./mvnw -B clean install -Dmaven.javadoc.skip=true -Dmaven.test.skip=true - fi + ./mvnw -B -T 1C clean install -Dmaven.javadoc.skip=true -Dmaven.test.skip=true fi - name: Build integrated tests @@ -153,14 +119,10 @@ jobs: run: | if mvnd --version > /dev/null 2>&1; then echo "Using mvnd for build integrated tests" - mvnd -B clean install -Pit -DskipTests -am -f ./shenyu-integrated-test/pom.xml + mvnd -B -T 1C clean install -Pit -DskipTests -am -f ./shenyu-integrated-test/pom.xml else echo "Falling back to maven wrapper for integrated tests" - if [[ "${{ runner.os }}" == "Windows" ]]; then - ./mvnw.cmd -B clean install -Pit -DskipTests -am -f ./shenyu-integrated-test/pom.xml - else - ./mvnw -B clean install -Pit -DskipTests -am -f ./shenyu-integrated-test/pom.xml - fi + ./mvnw -B -T 1C clean install -Pit -DskipTests -am -f ./shenyu-integrated-test/pom.xml fi - name: Build examples @@ -169,14 +131,10 @@ jobs: run: | if mvnd --version > /dev/null 2>&1; then echo "Using mvnd for build examples" - mvnd -B clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -am -f ./shenyu-examples/pom.xml + mvnd -B -T 1C clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -am -f ./shenyu-examples/pom.xml else echo "Falling back to maven wrapper for examples" - if [[ "${{ runner.os }}" == "Windows" ]]; then - ./mvnw.cmd -B clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -am -f ./shenyu-examples/pom.xml - else - ./mvnw -B clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -am -f ./shenyu-examples/pom.xml - fi + ./mvnw -B -T 1C clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -am -f ./shenyu-examples/pom.xml fi - name: Build k8s Cluster @@ -196,14 +154,10 @@ jobs: run: | if mvnd --version > /dev/null 2>&1; then echo "Using mvnd for running tests" - mvnd test -Pit -f ./shenyu-integrated-test/${{ matrix.case }}/pom.xml + mvnd -T 1C test -Pit -f ./shenyu-integrated-test/${{ matrix.case }}/pom.xml else echo "Falling back to maven wrapper for tests" - if [[ "${{ runner.os }}" == "Windows" ]]; then - ./mvnw.cmd test -Pit -f ./shenyu-integrated-test/${{ matrix.case }}/pom.xml - else - ./mvnw test -Pit -f ./shenyu-integrated-test/${{ matrix.case }}/pom.xml - fi + ./mvnw -T 1C test -Pit -f ./shenyu-integrated-test/${{ matrix.case }}/pom.xml fi continue-on-error: true diff --git a/.github/workflows/integrated-test.yml b/.github/workflows/integrated-test.yml index 9afc310f1c2f..dd9fa30a182f 100644 --- a/.github/workflows/integrated-test.yml +++ b/.github/workflows/integrated-test.yml @@ -75,53 +75,35 @@ jobs: - '!NOTICE' - '!.github/ISSUE_TEMPLATE/**' - '!.github/PULL_REQUEST_TEMPLATE' - - name: Restore ShenYu Maven Repos - if: steps.filter.outputs.integration == 'true' - uses: actions/cache/restore@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - uses: actions/setup-java@v4 if: steps.filter.outputs.integration == 'true' with: java-version: 17 distribution: "temurin" + cache: "maven" + cache-dependency-path: | + **/pom.xml - name: Install mvnd if: steps.filter.outputs.integration == 'true' shell: bash run: | MVND_VERSION=1.0.2 - if [[ "${{ runner.os }}" == "Windows" ]]; then - curl -sL https://downloads.apache.org/maven/mvnd/${MVND_VERSION}/maven-mvnd-${MVND_VERSION}-windows-amd64.zip -o mvnd.zip - unzip -q mvnd.zip - mkdir -p $HOME/.local - mv maven-mvnd-${MVND_VERSION}-windows-amd64 $HOME/.local/mvnd - echo "$HOME/.local/mvnd/bin" >> $GITHUB_PATH - echo "MVND_HOME=$HOME/.local/mvnd" >> $GITHUB_ENV - else - curl -sL https://downloads.apache.org/maven/mvnd/${MVND_VERSION}/maven-mvnd-${MVND_VERSION}-linux-amd64.zip -o mvnd.zip - unzip -q mvnd.zip - mkdir -p $HOME/.local - mv maven-mvnd-${MVND_VERSION}-linux-amd64 $HOME/.local/mvnd - echo "$HOME/.local/mvnd/bin" >> $GITHUB_PATH - echo "MVND_HOME=$HOME/.local/mvnd" >> $GITHUB_ENV - fi + curl -sL https://downloads.apache.org/maven/mvnd/${MVND_VERSION}/maven-mvnd-${MVND_VERSION}-linux-amd64.zip -o mvnd.zip + unzip -q mvnd.zip + mkdir -p $HOME/.local + mv maven-mvnd-${MVND_VERSION}-linux-amd64 $HOME/.local/mvnd + echo "$HOME/.local/mvnd/bin" >> $GITHUB_PATH + echo "MVND_HOME=$HOME/.local/mvnd" >> $GITHUB_ENV - name: Build with Maven if: steps.filter.outputs.integration == 'true' shell: bash run: | if mvnd --version > /dev/null 2>&1; then echo "Using mvnd for build" - mvnd -B clean install -Prelease,docker -Dmaven.javadoc.skip=true -Dmaven.test.skip=true + mvnd -B -T 1C clean install -Prelease,docker -Dmaven.javadoc.skip=true -Dmaven.test.skip=true else echo "Falling back to maven wrapper" - if [[ "${{ runner.os }}" == "Windows" ]]; then - ./mvnw.cmd -B clean install -Prelease,docker -Dmaven.javadoc.skip=true -Dmaven.test.skip=true - else - ./mvnw -B clean install -Prelease,docker -Dmaven.javadoc.skip=true -Dmaven.test.skip=true - fi + ./mvnw -B -T 1C clean install -Prelease,docker -Dmaven.javadoc.skip=true -Dmaven.test.skip=true fi - name: Build examples if: steps.filter.outputs.integration == 'true' @@ -129,14 +111,10 @@ jobs: run: | if mvnd --version > /dev/null 2>&1; then echo "Using mvnd for build examples" - mvnd -B clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -am -f ./shenyu-examples/pom.xml + mvnd -B -T 1C clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -am -f ./shenyu-examples/pom.xml else echo "Falling back to maven wrapper for examples" - if [[ "${{ runner.os }}" == "Windows" ]]; then - ./mvnw.cmd -B clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -am -f ./shenyu-examples/pom.xml - else - ./mvnw -B clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -am -f ./shenyu-examples/pom.xml - fi + ./mvnw -B -T 1C clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -am -f ./shenyu-examples/pom.xml fi - name: Build integrated tests if: steps.filter.outputs.integration == 'true' @@ -144,14 +122,10 @@ jobs: run: | if mvnd --version > /dev/null 2>&1; then echo "Using mvnd for build integrated tests" - mvnd -B clean install -Pit -DskipTests -f ./shenyu-integrated-test/pom.xml + mvnd -B -T 1C clean install -Pit -DskipTests -f ./shenyu-integrated-test/pom.xml else echo "Falling back to maven wrapper for integrated tests" - if [[ "${{ runner.os }}" == "Windows" ]]; then - ./mvnw.cmd -B clean install -Pit -DskipTests -f ./shenyu-integrated-test/pom.xml - else - ./mvnw -B clean install -Pit -DskipTests -f ./shenyu-integrated-test/pom.xml - fi + ./mvnw -B -T 1C clean install -Pit -DskipTests -f ./shenyu-integrated-test/pom.xml fi - name: Start docker compose if: steps.filter.outputs.integration == 'true' @@ -169,14 +143,10 @@ jobs: run: | if mvnd --version > /dev/null 2>&1; then echo "Using mvnd for running tests" - mvnd test -Pit -f ./shenyu-integrated-test/${{ matrix.case }}/pom.xml + mvnd -T 1C test -Pit -f ./shenyu-integrated-test/${{ matrix.case }}/pom.xml else echo "Falling back to maven wrapper for tests" - if [[ "${{ runner.os }}" == "Windows" ]]; then - ./mvnw.cmd test -Pit -f ./shenyu-integrated-test/${{ matrix.case }}/pom.xml - else - ./mvnw test -Pit -f ./shenyu-integrated-test/${{ matrix.case }}/pom.xml - fi + ./mvnw -T 1C test -Pit -f ./shenyu-integrated-test/${{ matrix.case }}/pom.xml fi continue-on-error: true - name: Check test result diff --git a/.github/workflows/k8s-examples-http.yml b/.github/workflows/k8s-examples-http.yml index 7f3cb06de7db..b2b3e4073214 100644 --- a/.github/workflows/k8s-examples-http.yml +++ b/.github/workflows/k8s-examples-http.yml @@ -64,53 +64,36 @@ jobs: cat /etc/rancher/k3s/k3s.yaml mkdir -p ~/.kube cp /etc/rancher/k3s/k3s.yaml ~/.kube/config - - name: Restore ShenYu Maven Repos - if: steps.filter.outputs.k8s-examples == 'true' - uses: actions/cache/restore@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - uses: actions/setup-java@v4 + - name: Setup Java and Maven cache if: steps.filter.outputs.k8s-examples == 'true' + uses: actions/setup-java@v4 with: java-version: 17 distribution: "temurin" + cache: "maven" + cache-dependency-path: | + **/pom.xml - name: Install mvnd if: steps.filter.outputs.k8s-examples == 'true' shell: bash run: | MVND_VERSION=1.0.2 - if [[ "${{ runner.os }}" == "Windows" ]]; then - curl -sL https://downloads.apache.org/maven/mvnd/${MVND_VERSION}/maven-mvnd-${MVND_VERSION}-windows-amd64.zip -o mvnd.zip - unzip -q mvnd.zip - mkdir -p $HOME/.local - mv maven-mvnd-${MVND_VERSION}-windows-amd64 $HOME/.local/mvnd - echo "$HOME/.local/mvnd/bin" >> $GITHUB_PATH - echo "MVND_HOME=$HOME/.local/mvnd" >> $GITHUB_ENV - else - curl -sL https://downloads.apache.org/maven/mvnd/${MVND_VERSION}/maven-mvnd-${MVND_VERSION}-linux-amd64.zip -o mvnd.zip - unzip -q mvnd.zip - mkdir -p $HOME/.local - mv maven-mvnd-${MVND_VERSION}-linux-amd64 $HOME/.local/mvnd - echo "$HOME/.local/mvnd/bin" >> $GITHUB_PATH - echo "MVND_HOME=$HOME/.local/mvnd" >> $GITHUB_ENV - fi + curl -sL https://downloads.apache.org/maven/mvnd/${MVND_VERSION}/maven-mvnd-${MVND_VERSION}-linux-amd64.zip -o mvnd.zip + unzip -q mvnd.zip + mkdir -p $HOME/.local + mv maven-mvnd-${MVND_VERSION}-linux-amd64 $HOME/.local/mvnd + echo "$HOME/.local/mvnd/bin" >> $GITHUB_PATH + echo "MVND_HOME=$HOME/.local/mvnd" >> $GITHUB_ENV - name: Build with Maven if: steps.filter.outputs.k8s-examples == 'true' shell: bash run: | if mvnd --version > /dev/null 2>&1; then echo "Using mvnd for build" - mvnd -B clean install -Prelease,docker -Dmaven.javadoc.skip=true -Dmaven.test.skip=true + mvnd -B -T 1C clean install -Prelease,docker -Dmaven.javadoc.skip=true -Dmaven.test.skip=true else echo "Falling back to maven wrapper" - if [[ "${{ runner.os }}" == "Windows" ]]; then - ./mvnw.cmd -B clean install -Prelease,docker -Dmaven.javadoc.skip=true -Dmaven.test.skip=true - else - ./mvnw -B clean install -Prelease,docker -Dmaven.javadoc.skip=true -Dmaven.test.skip=true - fi + ./mvnw -B -T 1C clean install -Prelease,docker -Dmaven.javadoc.skip=true -Dmaven.test.skip=true fi - name: Build examples if: steps.filter.outputs.k8s-examples == 'true' @@ -118,14 +101,10 @@ jobs: run: | if mvnd --version > /dev/null 2>&1; then echo "Using mvnd for build examples" - mvnd -B clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -f ./shenyu-examples/pom.xml + mvnd -B -T 1C clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -f ./shenyu-examples/pom.xml else echo "Falling back to maven wrapper for examples" - if [[ "${{ runner.os }}" == "Windows" ]]; then - ./mvnw.cmd -B clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -f ./shenyu-examples/pom.xml - else - ./mvnw -B clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -f ./shenyu-examples/pom.xml - fi + ./mvnw -B -T 1C clean install -Pexample -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -f ./shenyu-examples/pom.xml fi - name: Build k8s Cluster if: steps.filter.outputs.k8s-examples == 'true' From 27885929d5457f8be9085ba3a47dd81185a1aa54 Mon Sep 17 00:00:00 2001 From: liuhy Date: Tue, 16 Jun 2026 10:40:49 +0800 Subject: [PATCH 3/3] Require authorization for plugin data sync The namespace plugin sync endpoint can publish uploaded plugin JAR data to connected gateway nodes, so it needs the same plugin modify permission already required by the bulk sync path. This adds the missing method-level Shiro permission and locks the contract with a focused controller regression test. Constraint: Existing Shiro method authorization depends on @RequiresPermissions annotations. Rejected: Add a new permission name | existing syncPluginAll already uses system:plugin:modify for the same operation family. Confidence: high Scope-risk: narrow Directive: Keep single-plugin and bulk plugin sync endpoints on equivalent plugin sync permissions. Tested: ./mvnw -pl shenyu-admin -Dskip.checkstyle=true -DskipLicense=true -Dspotless.check.skip=true -Djacoco.skip=true -Dtest=NamespacePluginControllerTest test -q Tested: ./mvnw -pl shenyu-admin -Dskip.checkstyle=true -DskipLicense=true -Dspotless.check.skip=true -Djacoco.skip=true -Dtest=NamespacePluginControllerTest,DataPermissionControllerTest,SelectorControllerTest test -q Tested: ./mvnw -pl shenyu-admin -Dskip.checkstyle=true -DskipLicense=true -Dspotless.check.skip=true -Djacoco.skip=true test -q Tested: ./mvnw -pl shenyu-admin -DskipTests -Djacoco.skip=true verify -q Not-tested: End-to-end admin login and WebSocket sync against a running bootstrap. --- .../controller/NamespacePluginController.java | 1 + .../NamespacePluginControllerTest.java | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/NamespacePluginControllerTest.java diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/NamespacePluginController.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/NamespacePluginController.java index aba4a2eda4da..86eef6b1249d 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/NamespacePluginController.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/NamespacePluginController.java @@ -222,6 +222,7 @@ public ShenyuAdminResult syncPluginAll(@Valid @RequestBody final NamespaceSyncDT * @return {@linkplain ShenyuAdminResult} */ @PutMapping("/syncPluginData") + @RequiresPermissions("system:plugin:modify") public ShenyuAdminResult syncPluginData(@RequestParam("id") final String id) { return ShenyuAdminResult.success(syncDataService.syncPluginData(id) ? ShenyuResultMessage.SYNC_SUCCESS : ShenyuResultMessage.SYNC_FAIL); } diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/NamespacePluginControllerTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/NamespacePluginControllerTest.java new file mode 100644 index 000000000000..68f75b7d6029 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/NamespacePluginControllerTest.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.shenyu.admin.controller; + +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.junit.jupiter.api.Test; +import org.springframework.core.annotation.AnnotationUtils; + +import java.lang.reflect.Method; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Test cases for {@link NamespacePluginController}. + */ +public final class NamespacePluginControllerTest { + + @Test + public void shouldRequireModifyPermissionForSyncPluginData() throws NoSuchMethodException { + assertPermissions(NamespacePluginController.class.getMethod("syncPluginData", String.class), + "system:plugin:modify"); + } + + private void assertPermissions(final Method method, final String... expectedPermissions) { + RequiresPermissions permissions = AnnotationUtils.findAnnotation(method, RequiresPermissions.class); + if (Objects.isNull(permissions)) { + permissions = AnnotationUtils.findAnnotation(method.getDeclaringClass(), RequiresPermissions.class); + } + assertNotNull(permissions, method.getName() + " should declare @RequiresPermissions"); + assertArrayEquals(expectedPermissions, permissions.value(), method.getName() + " should declare the expected permissions"); + } +}