Repository: smallrye/smallrye-graphql
Issue: Typesafe client: inherit `@Header`/`@AuthorizationHeader` annotations

Annotations on the parent of a `@GraphQlClientApi` interface are not recognized.

Changed files:
client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/GraphQlClientBuilderImpl.java
client/implementation/src/test/java/test/unit/AuthorizationHeaderBehavior.java
client/implementation/src/test/java/test/unit/HeaderBehavior.java
client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/HeaderBuilder.java
client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/reflection/MethodInfo.java
client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/GraphQlClientProxy.java

diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/GraphQlClientBuilderImpl.java b/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/GraphQlClientBuilderImpl.java
index b0674489..e27f5bd3 100644
--- a/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/GraphQlClientBuilderImpl.java
+++ b/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/GraphQlClientBuilderImpl.java
@@ -42,7 +42,7 @@ public class GraphQlClientBuilderImpl implements GraphQlClientBuilder {
         WebTarget webTarget = client.target(resolveEndpoint(apiClass));
         GraphQlClientProxy graphQlClient = new GraphQlClientProxy(webTarget);
         return apiClass.cast(Proxy.newProxyInstance(apiClass.getClassLoader(), new Class<?>[] { apiClass },
-                (proxy, method, args) -> graphQlClient.invoke(MethodInfo.of(method, args))));
+                (proxy, method, args) -> graphQlClient.invoke(apiClass, MethodInfo.of(method, args))));
     }
 
     private void readConfig(GraphQlClientApi annotation) {
diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/GraphQlClientProxy.java b/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/GraphQlClientProxy.java
index c6e3b6a2..bd200d63 100644
--- a/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/GraphQlClientProxy.java
+++ b/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/GraphQlClientProxy.java
@@ -41,8 +41,8 @@ class GraphQlClientProxy {
         this.target = target;
     }
 
-    Object invoke(MethodInfo method) {
-        MultivaluedMap<String, Object> headers = new HeaderBuilder(method).build();
+    Object invoke(Class<?> api, MethodInfo method) {
+        MultivaluedMap<String, Object> headers = new HeaderBuilder(api, method).build();
         String request = request(method);
 
         log.info("request graphql: {}", request);
diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/HeaderBuilder.java b/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/HeaderBuilder.java
index 84ff32fa..85d7d721 100644
--- a/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/HeaderBuilder.java
+++ b/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/HeaderBuilder.java
@@ -20,14 +20,16 @@ import io.smallrye.graphql.client.typesafe.impl.reflection.MethodResolver;
 import io.smallrye.graphql.client.typesafe.impl.reflection.TypeInfo;
 
 public class HeaderBuilder {
+    private final Class<?> api;
     private final MethodInfo method;
 
-    public HeaderBuilder(MethodInfo method) {
+    public HeaderBuilder(Class<?> api, MethodInfo method) {
+        this.api = api;
         this.method = method;
     }
 
     public MultivaluedMap<String, Object> build() {
-        MultivaluedMap<String, Object> headers = method.getResolvedAnnotations(Header.class)
+        MultivaluedMap<String, Object> headers = method.getResolvedAnnotations(api, Header.class)
                 .map(header -> new SimpleEntry<>(header.name(), resolveValue(header)))
                 .collect(toMultivaluedMap());
         method.parameters()
@@ -36,7 +38,7 @@ public class HeaderBuilder {
                     Header header = parameter.getAnnotations(Header.class)[0];
                     headers.add(header.name(), parameter.getValue());
                 });
-        method.getResolvedAnnotations(AuthorizationHeader.class)
+        method.getResolvedAnnotations(api, AuthorizationHeader.class)
                 .findFirst()
                 .map(header -> resolveAuthHeader(method.getDeclaringType(), header))
                 .ifPresent(auth -> headers.add("Authorization", auth));
diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/reflection/MethodInfo.java b/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/reflection/MethodInfo.java
index 71a6d0c2..4b5dbbab 100644
--- a/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/reflection/MethodInfo.java
+++ b/client/implementation/src/main/java/io/smallrye/graphql/client/typesafe/impl/reflection/MethodInfo.java
@@ -104,12 +104,20 @@ public class MethodInfo {
         return type;
     }
 
-    public <A extends Annotation> Stream<A> getResolvedAnnotations(Class<A> type) {
+    public <A extends Annotation> Stream<A> getResolvedAnnotations(Class<?> declaring, Class<A> type) {
         return Stream.concat(resolveAnnotations(method, type),
-                resolveAnnotations(method.getDeclaringClass(), type))
+                resolveInheritedAnnotations(declaring, type))
                 .filter(Objects::nonNull);
     }
 
+    private <A extends Annotation> Stream<A> resolveInheritedAnnotations(Class<?> declaring, Class<A> type) {
+        Stream<A> stream = resolveAnnotations(declaring, type);
+        for (Class<?> i : declaring.getInterfaces()) {
+            stream = Stream.concat(stream, resolveInheritedAnnotations(i, type));
+        }
+        return stream;
+    }
+
     private static <A extends Annotation> Stream<A> resolveAnnotations(AnnotatedElement annotatedElement, Class<A> type) {
         return Stream.concat(Stream.of(annotatedElement.getAnnotationsByType(type)),
                 resolveStereotypes(annotatedElement.getAnnotations(), type));
diff --git a/client/implementation/src/test/java/test/unit/AuthorizationHeaderBehavior.java b/client/implementation/src/test/java/test/unit/AuthorizationHeaderBehavior.java
index d062450d..eed17cdb 100644
--- a/client/implementation/src/test/java/test/unit/AuthorizationHeaderBehavior.java
+++ b/client/implementation/src/test/java/test/unit/AuthorizationHeaderBehavior.java
@@ -85,7 +85,10 @@ public class AuthorizationHeaderBehavior {
 
     @GraphQlClientApi
     @AuthorizationHeader(confPrefix = "*")
-    public interface InheritedAuthorizationHeadersApi {
+    public interface InheritedAuthorizationHeadersApi extends BaseAuthorizationHeadersApi {
+    }
+
+    public interface BaseAuthorizationHeadersApi {
         @SuppressWarnings("UnusedReturnValue")
         String greeting();
     }
@@ -104,7 +107,7 @@ public class AuthorizationHeaderBehavior {
 
     @GraphQlClientApi(configKey = "foo")
     @AuthorizationHeader
-    public interface InheritedConfigKeyAuthorizationHeadersApi {
+    public interface ConfigKeyAuthorizationHeadersApi {
         @SuppressWarnings("UnusedReturnValue")
         String greeting();
     }
@@ -113,8 +116,8 @@ public class AuthorizationHeaderBehavior {
     public void shouldAddInheritedConfigKeyAuthorizationHeader() {
         withCredentials("foo/mp-graphql/", () -> {
             fixture.returnsData("'greeting':'dummy-greeting'");
-            InheritedConfigKeyAuthorizationHeadersApi api = fixture.builder()
-                    .build(InheritedConfigKeyAuthorizationHeadersApi.class);
+            ConfigKeyAuthorizationHeadersApi api = fixture.builder()
+                    .build(ConfigKeyAuthorizationHeadersApi.class);
 
             api.greeting();
 
@@ -136,7 +139,7 @@ public class AuthorizationHeaderBehavior {
     }
 
     @Test
-    public void shouldAddAuthenticatedHeader() {
+    public void shouldAddStereotypedHeader() {
         withCredentials("", () -> {
             fixture.returnsData("'greeting':'dummy-greeting'");
             AuthenticatedHeaderApi api = fixture.builder().build(AuthenticatedHeaderApi.class);
diff --git a/client/implementation/src/test/java/test/unit/HeaderBehavior.java b/client/implementation/src/test/java/test/unit/HeaderBehavior.java
index 0d66e5f1..be8da39e 100644
--- a/client/implementation/src/test/java/test/unit/HeaderBehavior.java
+++ b/client/implementation/src/test/java/test/unit/HeaderBehavior.java
@@ -525,4 +525,40 @@ public class HeaderBehavior {
 
         then(fixture.sentHeader("H")).isEqualTo("V");
     }
+
+    @GraphQlClientApi
+    @Header(name = "H1", constant = "V1")
+    @Header(name = "overwrite", constant = "sub")
+    interface InheritingHeadersApi extends Base, SideBase {
+    }
+
+    @Header(name = "H2", constant = "V2")
+    @Header(name = "overwrite", constant = "super")
+    interface Base extends SuperBase {
+        @Header(name = "H3", constant = "V3")
+        String greeting();
+    }
+
+    @Header(name = "H4", constant = "V4")
+    interface SideBase {
+    }
+
+    @Header(name = "H5", constant = "V5")
+    interface SuperBase {
+    }
+
+    @Test
+    public void shouldAddExtendedHeaders() {
+        fixture.returnsData("'greeting':'dummy-greeting'");
+        InheritingHeadersApi api = fixture.builder().build(InheritingHeadersApi.class);
+
+        api.greeting();
+
+        then(fixture.sentHeader("H1")).isEqualTo("V1");
+        then(fixture.sentHeader("H2")).isEqualTo("V2");
+        then(fixture.sentHeader("H3")).isEqualTo("V3");
+        then(fixture.sentHeader("H4")).isEqualTo("V4");
+        then(fixture.sentHeader("H5")).isEqualTo("V5");
+        then(fixture.sentHeader("overwrite")).isEqualTo("sub");
+    }
 }