From c9da3c1a016890c974662bf9ce649878f0df5218 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:07:28 -0500 Subject: [PATCH 1/3] chore: Fixed issue with resolve of "value" field - Missing Mustache resolution - Couldn't be used to set headers like "const" field --- src/main/java/io/naftiko/engine/Resolver.java | 11 +++- .../engine/exposes/ApiResourceRestlet.java | 63 +++++++++++-------- .../io/naftiko/spec/InputParameterSpec.java | 5 +- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/main/java/io/naftiko/engine/Resolver.java b/src/main/java/io/naftiko/engine/Resolver.java index 80c04e5..f435038 100644 --- a/src/main/java/io/naftiko/engine/Resolver.java +++ b/src/main/java/io/naftiko/engine/Resolver.java @@ -142,6 +142,13 @@ public static Object resolveInputParameterFromRequest(InputParameterSpec spec, R /** * Apply a list of input parameter specs to a client request (headers and query params). + * + * Resolution priority for parameter values: + * 1. 'value' field - resolved with Mustache template syntax ({{paramName}}) for dynamic resolution + * 2. 'const' field - used as-is, no template resolution applied + * 3. 'template' field - resolved with Mustache syntax + * 4. Parameters map - direct lookup by parameter name + * 5. Environment variables - for 'environment' location */ public static void resolveInputParametersToRequest(Request clientRequest, List specs, Map parameters) { @@ -156,8 +163,10 @@ public static void resolveInputParametersToRequest(Request clientRequest, Object val = null; if (spec.getValue() != null) { - val = spec.getValue(); + // Resolve Mustache templates in value, allowing dynamic parameter resolution + val = Resolver.resolveMustacheTemplate(spec.getValue(), parameters); } else if (spec.getConstant() != null) { + // Use constant value as-is (no template resolution) val = spec.getConstant(); } else if (spec.getTemplate() != null) { val = Resolver.resolveMustacheTemplate(spec.getTemplate(), parameters); diff --git a/src/main/java/io/naftiko/engine/exposes/ApiResourceRestlet.java b/src/main/java/io/naftiko/engine/exposes/ApiResourceRestlet.java index f0565b6..6fc7437 100644 --- a/src/main/java/io/naftiko/engine/exposes/ApiResourceRestlet.java +++ b/src/main/java/io/naftiko/engine/exposes/ApiResourceRestlet.java @@ -390,33 +390,42 @@ private boolean handleFromForwardSpec(Request request, Response response) { if (httpAdapter.getHttpClientSpec().getNamespace() .equals(forwardSpec.getTargetNamespace())) { - // Prepare the HTTP client request - String path = (String) request.getAttributes().get("path"); - String targetRef = httpAdapter.getHttpClientSpec().getBaseUri() + path; - Request clientRequest = new Request(request.getMethod(), targetRef); - clientRequest.setEntity(request.getEntity()); - - // Copy trusted headers from the original request to the client request - copyTrustedHeaders(request, clientRequest, - getResourceSpec().getForward().getTrustedHeaders()); - - // Resolve HTTP client input parameters for authentication template resolution - Map parameters = new ConcurrentHashMap<>(); - Resolver.resolveInputParametersToRequest(clientRequest, - httpAdapter.getHttpClientSpec().getInputParameters(), parameters); - - // Set any authentication needed on the client request - Response clientResponse = new Response(clientRequest); - httpAdapter.setChallengeResponse(request, clientRequest, - clientRequest.getResourceRef().toString(), parameters); - httpAdapter.setHeaders(clientRequest); - - // Send the request to the target endpoint - httpAdapter.getHttpClient().handle(clientRequest, clientResponse); - response.setStatus(clientResponse.getStatus()); - response.setEntity(clientResponse.getEntity()); - response.commit(); - return true; + try { + // Prepare the HTTP client request + String path = (String) request.getAttributes().get("path"); + String targetRef = httpAdapter.getHttpClientSpec().getBaseUri() + path; + Request clientRequest = new Request(request.getMethod(), targetRef); + clientRequest.setEntity(request.getEntity()); + + // Copy trusted headers from the original request to the client request + copyTrustedHeaders(request, clientRequest, + getResourceSpec().getForward().getTrustedHeaders()); + + // Prepare parameters map for template resolution + Map parameters = new ConcurrentHashMap<>(); + + // Set any authentication needed on the client request + Response clientResponse = new Response(clientRequest); + httpAdapter.setChallengeResponse(request, clientRequest, + clientRequest.getResourceRef().toString(), parameters); + httpAdapter.setHeaders(clientRequest); + + // Apply HTTP client input parameters last to ensure they take precedence + Resolver.resolveInputParametersToRequest(clientRequest, + httpAdapter.getHttpClientSpec().getInputParameters(), parameters); + + // Send the request to the target endpoint + httpAdapter.getHttpClient().handle(clientRequest, clientResponse); + response.setStatus(clientResponse.getStatus()); + response.setEntity(clientResponse.getEntity()); + response.commit(); + return true; + } catch (Exception e) { + response.setStatus(Status.SERVER_ERROR_INTERNAL); + response.setEntity("Error while handling an HTTP client call\n\n" + e.toString(), + MediaType.TEXT_PLAIN); + return true; + } } } } diff --git a/src/main/java/io/naftiko/spec/InputParameterSpec.java b/src/main/java/io/naftiko/spec/InputParameterSpec.java index 418edb9..9dbb52f 100644 --- a/src/main/java/io/naftiko/spec/InputParameterSpec.java +++ b/src/main/java/io/naftiko/spec/InputParameterSpec.java @@ -28,8 +28,9 @@ public class InputParameterSpec extends StructureSpec { private volatile String template; /** - * Value of the parameter. Can be a static value or an expression resolved from the execution - * context (e.g. {@code $this.namespace.param}). + * Value of the parameter. Supports Mustache template syntax ({{paramName}}) for dynamic + * resolution from the execution context. Provides an alternative to 'const' that allows + * parameter variable substitution. Takes precedence over 'const'. */ private volatile String value; From 7fb10e56fab26db784f92cedec8c33f29b0ea70c Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:53:41 -0500 Subject: [PATCH 2/3] chore: added non regression test --- .../CapabilityForwardValueFieldTest.java | 170 ++++++++++++++++++ .../http-forward-value-capability.yaml | 31 ++++ 2 files changed, 201 insertions(+) create mode 100644 src/test/java/io/naftiko/engine/CapabilityForwardValueFieldTest.java create mode 100644 src/test/resources/http-forward-value-capability.yaml diff --git a/src/test/java/io/naftiko/engine/CapabilityForwardValueFieldTest.java b/src/test/java/io/naftiko/engine/CapabilityForwardValueFieldTest.java new file mode 100644 index 0000000..e52c1ee --- /dev/null +++ b/src/test/java/io/naftiko/engine/CapabilityForwardValueFieldTest.java @@ -0,0 +1,170 @@ +/** + * Copyright 2025-2026 Naftiko + * + * 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 io.naftiko.engine; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.naftiko.Capability; +import io.naftiko.engine.consumes.HttpClientAdapter; +import io.naftiko.engine.exposes.ApiServerAdapter; +import io.naftiko.spec.NaftikoSpec; +import io.naftiko.spec.InputParameterSpec; +import io.naftiko.spec.exposes.ApiServerResourceSpec; +import io.naftiko.spec.exposes.ApiServerSpec; +import org.restlet.Request; +import org.restlet.data.Method; +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Non-regression test for the value field support with Mustache templates in forward spec. + * Tests that input parameters with "value" field are properly resolved for API forwarding. + */ +public class CapabilityForwardValueFieldTest { + + private Capability capability; + private ApiServerSpec serverSpec; + private ApiServerResourceSpec resourceSpec; + + @BeforeEach + public void setUp() throws Exception { + String resourcePath = "src/test/resources/http-forward-value-capability.yaml"; + File file = new File(resourcePath); + assertTrue(file.exists(), "Capability file should exist at " + resourcePath); + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + NaftikoSpec spec = mapper.readValue(file, NaftikoSpec.class); + + capability = new Capability(spec); + + ApiServerAdapter adapter = (ApiServerAdapter) capability.getServerAdapters().get(0); + serverSpec = (ApiServerSpec) adapter.getSpec(); + resourceSpec = serverSpec.getResources().get(0); + } + + @Test + public void testValueFieldResolutionInForwardSpec() throws Exception { + // Verify the HTTP client adapter exists and has input parameters configured + HttpClientAdapter httpAdapter = null; + for (var adapter : capability.getClientAdapters()) { + if (adapter instanceof HttpClientAdapter) { + HttpClientAdapter httpClientAdapter = (HttpClientAdapter) adapter; + if (httpClientAdapter.getHttpClientSpec().getNamespace().equals("external")) { + httpAdapter = httpClientAdapter; + break; + } + } + } + + assertNotNull(httpAdapter, "HTTP client adapter for namespace 'external' should exist"); + + // Verify input parameters are configured + assertFalse(httpAdapter.getHttpClientSpec().getInputParameters().isEmpty(), + "HTTP client spec should have input parameters configured"); + + // Find the value and const input parameters + InputParameterSpec valueParam = null; + InputParameterSpec constParam = null; + + for (InputParameterSpec param : httpAdapter.getHttpClientSpec().getInputParameters()) { + if ("X-API-Version".equals(param.getName())) { + valueParam = param; + } else if ("X-API-Key".equals(param.getName())) { + constParam = param; + } + } + + assertNotNull(valueParam, "X-API-Version parameter with 'value' field should exist"); + assertNotNull(constParam, "X-API-Key parameter with 'const' field should exist"); + + assertEquals("2026-03-03", valueParam.getValue(), + "X-API-Version 'value' field should be set to '2026-03-03'"); + assertEquals("secret-key-123", constParam.getConstant(), + "X-API-Key 'const' field should be set to 'secret-key-123'"); + } + + @Test + public void testValueFieldAppliedToRequest() throws Exception { + // Find the HTTP client adapter + HttpClientAdapter httpAdapter = null; + for (var adapter : capability.getClientAdapters()) { + if (adapter instanceof HttpClientAdapter) { + HttpClientAdapter httpClientAdapter = (HttpClientAdapter) adapter; + if (httpClientAdapter.getHttpClientSpec().getNamespace().equals("external")) { + httpAdapter = httpClientAdapter; + break; + } + } + } + + assertNotNull(httpAdapter, "HTTP client adapter should exist"); + + // Create a test request + Request clientRequest = new Request(Method.GET, "https://api.example.com/v1/test"); + + // Apply input parameters to the request + Map parameters = new ConcurrentHashMap<>(); + Resolver.resolveInputParametersToRequest(clientRequest, + httpAdapter.getHttpClientSpec().getInputParameters(), parameters); + + // Verify both headers are set + String apiVersionHeader = clientRequest.getHeaders().getFirstValue("X-API-Version", true); + String apiKeyHeader = clientRequest.getHeaders().getFirstValue("X-API-Key", true); + + assertNotNull(apiVersionHeader, "X-API-Version header should be set"); + assertEquals("2026-03-03", apiVersionHeader, + "X-API-Version header should have the value from 'value' field"); + + assertNotNull(apiKeyHeader, "X-API-Key header should be set"); + assertEquals("secret-key-123", apiKeyHeader, + "X-API-Key header should have the value from 'const' field"); + + // Verify that both values were added to parameters map for template resolution + assertTrue(parameters.containsKey("X-API-Version"), + "X-API-Version should be in the parameters map"); + assertTrue(parameters.containsKey("X-API-Key"), + "X-API-Key should be in the parameters map"); + } + + @Test + public void testValueFieldWithMustacheTemplate() throws Exception { + // This test verifies that the value field supports Mustache template syntax + // by testing the Resolver directly with a value field that has template syntax + + InputParameterSpec spec = new InputParameterSpec(); + spec.setName("X-Request-Id"); + spec.setIn("header"); + spec.setValue("{{requestId}}"); // Value with Mustache template + + Request request = new Request(Method.GET, "https://api.example.com/v1/test"); + Map parameters = new ConcurrentHashMap<>(); + parameters.put("requestId", "req-12345"); + + // Apply the input parameter + Resolver.resolveInputParametersToRequest(request, + java.util.Collections.singletonList(spec), parameters); + + // Verify the header was set with the resolved template value + String headerValue = request.getHeaders().getFirstValue("X-Request-Id", true); + assertNotNull(headerValue, "X-Request-Id header should be set"); + assertEquals("req-12345", headerValue, + "X-Request-Id header should have the resolved Mustache template value"); + } +} diff --git a/src/test/resources/http-forward-value-capability.yaml b/src/test/resources/http-forward-value-capability.yaml new file mode 100644 index 0000000..7b73907 --- /dev/null +++ b/src/test/resources/http-forward-value-capability.yaml @@ -0,0 +1,31 @@ +naftiko: "0.4" +info: + label: "HTTP Forward with Value Field Test" + description: "Test capability for forward spec with value field supporting Mustache templates" + created: "2026-03-03" + modified: "2026-03-03" + +capability: + exposes: + - type: api + address: localhost + port: 0 + namespace: sample + resources: + - path: "/forward/{{path}}" + description: "Forward requests with value field parameter" + forward: + targetNamespace: external + + consumes: + - type: http + description: "External API endpoint" + namespace: external + baseUri: "https://api.example.com/v1/" + inputParameters: + - name: "X-API-Version" + in: "header" + value: "2026-03-03" + - name: "X-API-Key" + in: "header" + const: "secret-key-123" From 7eb5f467458d268ff6becd9e71a6304cd79b351d Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:57:52 -0500 Subject: [PATCH 3/3] chore: resolve value params before auth and keep bearer auth Restlet-compliant - Fix forward flow ordering so client inputParameters (including env-backed values) are resolved before authentication templates. - Preserve value-based headers by preventing default header application from overwriting non-const values. - Keep bearer auth compatible with Restlet by using ChallengeResponse (no direct Authorization header injection). - Add non-regression tests for: --- src/main/java/io/naftiko/engine/Resolver.java | 6 +- .../engine/consumes/HttpClientAdapter.java | 4 +- .../engine/exposes/ApiResourceRestlet.java | 9 +- .../schemas/tutorial/step3a-encapsulate.yml | 2 +- .../schemas/tutorial/step3b-encapsulate.yml | 2 +- .../schemas/tutorial/step3c-encapsulate.yml | 2 +- .../schemas/tutorial/step4a-structure.yml | 2 +- .../schemas/tutorial/step4b-structure.yml | 2 +- ...apabilityForwardHeaderIntegrationTest.java | 126 ++++++++++++++++++ .../consumes/HttpClientAdapterAuthTest.java | 51 +++++++ .../HttpClientAdapterHeadersTest.java | 71 ++++++++++ 11 files changed, 264 insertions(+), 13 deletions(-) create mode 100644 src/test/java/io/naftiko/engine/CapabilityForwardHeaderIntegrationTest.java create mode 100644 src/test/java/io/naftiko/engine/consumes/HttpClientAdapterAuthTest.java create mode 100644 src/test/java/io/naftiko/engine/consumes/HttpClientAdapterHeadersTest.java diff --git a/src/main/java/io/naftiko/engine/Resolver.java b/src/main/java/io/naftiko/engine/Resolver.java index f435038..f1d124b 100644 --- a/src/main/java/io/naftiko/engine/Resolver.java +++ b/src/main/java/io/naftiko/engine/Resolver.java @@ -13,6 +13,7 @@ */ package io.naftiko.engine; +import java.util.HashMap; import java.util.List; import java.util.Map; import org.restlet.Request; @@ -55,7 +56,8 @@ public static String resolveMustacheTemplate(String template, Map(parameters)); } /** @@ -178,7 +180,7 @@ public static void resolveInputParametersToRequest(Request clientRequest, if (val == null) { continue; - }else{ + } else if (parameters != null) { parameters.put(spec.getName(), val); } diff --git a/src/main/java/io/naftiko/engine/consumes/HttpClientAdapter.java b/src/main/java/io/naftiko/engine/consumes/HttpClientAdapter.java index e8e97b2..61f5a69 100644 --- a/src/main/java/io/naftiko/engine/consumes/HttpClientAdapter.java +++ b/src/main/java/io/naftiko/engine/consumes/HttpClientAdapter.java @@ -73,7 +73,7 @@ public HttpClientOperationSpec getOperationSpec(String operationName) { public void setHeaders(Request request) { // Set any default headers from the input parameters for (InputParameterSpec param : getHttpClientSpec().getInputParameters()) { - if ("header".equals(param.getIn())) { + if ("header".equalsIgnoreCase(param.getIn()) && param.getConstant() != null) { request.getHeaders().set(param.getName(), param.getConstant()); } } @@ -120,7 +120,7 @@ public void setChallengeResponse(Request serverRequest, Request clientRequest, S (BearerAuthenticationSpec) authenticationSpec; challengeResponse = new ChallengeResponse(ChallengeScheme.HTTP_OAUTH_BEARER); challengeResponse.setRawValue( - Resolver.resolveMustacheTemplate(bearerAuth.getToken(), parameters)); + Resolver.resolveMustacheTemplate(bearerAuth.getToken(), parameters)); clientRequest.setChallengeResponse(challengeResponse); break; diff --git a/src/main/java/io/naftiko/engine/exposes/ApiResourceRestlet.java b/src/main/java/io/naftiko/engine/exposes/ApiResourceRestlet.java index 6fc7437..5074033 100644 --- a/src/main/java/io/naftiko/engine/exposes/ApiResourceRestlet.java +++ b/src/main/java/io/naftiko/engine/exposes/ApiResourceRestlet.java @@ -403,16 +403,17 @@ private boolean handleFromForwardSpec(Request request, Response response) { // Prepare parameters map for template resolution Map parameters = new ConcurrentHashMap<>(); + + // Resolve client input parameters first so authentication templates + // (e.g. bearer token from environment) can be resolved correctly + Resolver.resolveInputParametersToRequest(clientRequest, + httpAdapter.getHttpClientSpec().getInputParameters(), parameters); // Set any authentication needed on the client request Response clientResponse = new Response(clientRequest); httpAdapter.setChallengeResponse(request, clientRequest, clientRequest.getResourceRef().toString(), parameters); httpAdapter.setHeaders(clientRequest); - - // Apply HTTP client input parameters last to ensure they take precedence - Resolver.resolveInputParametersToRequest(clientRequest, - httpAdapter.getHttpClientSpec().getInputParameters(), parameters); // Send the request to the target endpoint httpAdapter.getHttpClient().handle(clientRequest, clientResponse); diff --git a/src/main/resources/schemas/tutorial/step3a-encapsulate.yml b/src/main/resources/schemas/tutorial/step3a-encapsulate.yml index 6d015a3..2272466 100644 --- a/src/main/resources/schemas/tutorial/step3a-encapsulate.yml +++ b/src/main/resources/schemas/tutorial/step3a-encapsulate.yml @@ -2,7 +2,7 @@ --- naftiko: "0.4" info: - label: "Tutorial - Step 3a - Forwarding API Resource" + label: "Tutorial - Step 3a - Encapsulating HTTP source" description: "This is a sample capability specification to demonstrate the features of Naftiko" tags: - Naftiko diff --git a/src/main/resources/schemas/tutorial/step3b-encapsulate.yml b/src/main/resources/schemas/tutorial/step3b-encapsulate.yml index 8e2229c..7b62d8f 100644 --- a/src/main/resources/schemas/tutorial/step3b-encapsulate.yml +++ b/src/main/resources/schemas/tutorial/step3b-encapsulate.yml @@ -2,7 +2,7 @@ --- naftiko: "0.4" info: - label: "Tutorial - Step 3b - Forwarding API Resource" + label: "Tutorial - Step 3b - Encapsulating HTTP source" description: "This is a sample capability specification to demonstrate the features of Naftiko" tags: - Naftiko diff --git a/src/main/resources/schemas/tutorial/step3c-encapsulate.yml b/src/main/resources/schemas/tutorial/step3c-encapsulate.yml index 840dd50..133eac0 100644 --- a/src/main/resources/schemas/tutorial/step3c-encapsulate.yml +++ b/src/main/resources/schemas/tutorial/step3c-encapsulate.yml @@ -2,7 +2,7 @@ --- naftiko: "0.4" info: - label: "Tutorial - Step 3b - Forwarding API Resource" + label: "Tutorial - Step 3b - Encapsulating HTTP source" description: "This is a sample capability specification to demonstrate the features of Naftiko" tags: - Naftiko diff --git a/src/main/resources/schemas/tutorial/step4a-structure.yml b/src/main/resources/schemas/tutorial/step4a-structure.yml index fee15ac..219cec0 100644 --- a/src/main/resources/schemas/tutorial/step4a-structure.yml +++ b/src/main/resources/schemas/tutorial/step4a-structure.yml @@ -2,7 +2,7 @@ --- naftiko: "0.4" info: - label: "Tutorial - Step 3b - Forwarding API Resource" + label: "Tutorial - Step 4a - Structured API resource" description: "This is a sample capability specification to demonstrate the features of Naftiko" tags: - Naftiko diff --git a/src/main/resources/schemas/tutorial/step4b-structure.yml b/src/main/resources/schemas/tutorial/step4b-structure.yml index b8f9a3e..9941128 100644 --- a/src/main/resources/schemas/tutorial/step4b-structure.yml +++ b/src/main/resources/schemas/tutorial/step4b-structure.yml @@ -3,7 +3,7 @@ naftiko: "0.4" info: - label: "Tutorial - Step 3b - Forwarding API Resource" + label: "Tutorial - Step 4b - Structured API resource" description: "This is a sample capability specification to demonstrate the features of Naftiko" tags: - Naftiko diff --git a/src/test/java/io/naftiko/engine/CapabilityForwardHeaderIntegrationTest.java b/src/test/java/io/naftiko/engine/CapabilityForwardHeaderIntegrationTest.java new file mode 100644 index 0000000..67e5cd1 --- /dev/null +++ b/src/test/java/io/naftiko/engine/CapabilityForwardHeaderIntegrationTest.java @@ -0,0 +1,126 @@ +/** + * Copyright 2025-2026 Naftiko + * + * 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 io.naftiko.engine; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.data.MediaType; +import org.restlet.data.Status; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import io.naftiko.Capability; +import io.naftiko.engine.exposes.ApiResourceRestlet; +import io.naftiko.engine.exposes.ApiServerAdapter; +import io.naftiko.spec.NaftikoSpec; +import io.naftiko.spec.exposes.ApiServerResourceSpec; +import io.naftiko.spec.exposes.ApiServerSpec; + +public class CapabilityForwardHeaderIntegrationTest { + + @Test + public void forwardShouldSetNotionVersionHeaderFromValueField() throws Exception { + AtomicReference receivedVersion = new AtomicReference<>(); + + HttpServer mockServer = HttpServer.create(new InetSocketAddress(0), 0); + int mockPort = mockServer.getAddress().getPort(); + mockServer.createContext("/v1/pages", new HttpHandler() { + @Override + public void handle(HttpExchange exchange) { + try { + receivedVersion.set(exchange.getRequestHeaders().getFirst("Notion-Version")); + byte[] body = "{\"ok\":true}".getBytes(); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, body.length); + exchange.getResponseBody().write(body); + } catch (Exception ignored) { + } finally { + try { + exchange.close(); + } catch (Exception ignored) { + } + } + } + }); + mockServer.start(); + + try { + String yaml = """ + naftiko: \"0.4\" + capability: + exposes: + - type: \"api\" + address: \"localhost\" + port: 0 + namespace: \"sample\" + resources: + - path: \"/notion/{{path}}\" + forward: + targetNamespace: notion + trustedHeaders: + - Notion-Version + + consumes: + - type: \"http\" + namespace: \"notion\" + baseUri: \"http://localhost:%d/v1/\" + inputParameters: + - name: \"Notion-Version\" + in: \"header\" + value: \"2025-09-03\" + """.formatted(mockPort); + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + NaftikoSpec spec = mapper.readValue(yaml, NaftikoSpec.class); + + Capability capability = new Capability(spec); + ApiServerAdapter adapter = (ApiServerAdapter) capability.getServerAdapters().get(0); + ApiServerSpec serverSpec = (ApiServerSpec) adapter.getSpec(); + ApiServerResourceSpec resourceSpec = serverSpec.getResources().get(0); + ApiResourceRestlet restlet = new ApiResourceRestlet(capability, serverSpec, resourceSpec); + + Request request = new Request(org.restlet.data.Method.GET, + "http://localhost/notion/pages"); + request.getAttributes().put("path", "pages"); + Response response = new Response(request); + + Method forwardMethod = ApiResourceRestlet.class.getDeclaredMethod("handleFromForwardSpec", + Request.class, Response.class); + forwardMethod.setAccessible(true); + boolean handled = (boolean) forwardMethod.invoke(restlet, request, response); + + assertTrue(handled, "Forward request should be handled"); + assertEquals(Status.SUCCESS_OK, response.getStatus(), "Forward response should be 200"); + assertNotNull(response.getEntity(), "Forward response should include downstream entity"); + assertEquals(MediaType.APPLICATION_JSON, response.getEntity().getMediaType(), + "Forward response should be json"); + + assertEquals("2025-09-03", receivedVersion.get(), + "Notion-Version should be set from inputParameters.value in forward mode"); + } finally { + mockServer.stop(0); + } + } +} diff --git a/src/test/java/io/naftiko/engine/consumes/HttpClientAdapterAuthTest.java b/src/test/java/io/naftiko/engine/consumes/HttpClientAdapterAuthTest.java new file mode 100644 index 0000000..7d364ea --- /dev/null +++ b/src/test/java/io/naftiko/engine/consumes/HttpClientAdapterAuthTest.java @@ -0,0 +1,51 @@ +/** + * Copyright 2025-2026 Naftiko + * + * 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 io.naftiko.engine.consumes; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.junit.jupiter.api.Test; +import org.restlet.Request; +import org.restlet.data.Method; +import io.naftiko.spec.consumes.BearerAuthenticationSpec; +import io.naftiko.spec.consumes.HttpClientSpec; + +public class HttpClientAdapterAuthTest { + + @Test + public void bearerAuthenticationShouldSetAuthorizationHeader() { + HttpClientSpec spec = new HttpClientSpec("notion", "https://api.notion.com/v1/", null); + + BearerAuthenticationSpec authentication = new BearerAuthenticationSpec(); + authentication.setType("bearer"); + authentication.setToken("{{notion_api_key}}"); + spec.setAuthentication(authentication); + + HttpClientAdapter adapter = new HttpClientAdapter(null, spec); + Request clientRequest = new Request(Method.GET, "https://api.notion.com/v1/pages"); + + Map parameters = new ConcurrentHashMap<>(); + parameters.put("notion_api_key", "ntn_test_abc123"); + + adapter.setChallengeResponse(null, clientRequest, + clientRequest.getResourceRef().toString(), parameters); + + assertNotNull(clientRequest.getChallengeResponse(), + "Bearer auth should use Restlet ChallengeResponse"); + assertEquals("ntn_test_abc123", clientRequest.getChallengeResponse().getRawValue(), + "Bearer token should be set as challenge raw value"); + } +} diff --git a/src/test/java/io/naftiko/engine/consumes/HttpClientAdapterHeadersTest.java b/src/test/java/io/naftiko/engine/consumes/HttpClientAdapterHeadersTest.java new file mode 100644 index 0000000..166bf8c --- /dev/null +++ b/src/test/java/io/naftiko/engine/consumes/HttpClientAdapterHeadersTest.java @@ -0,0 +1,71 @@ +/** + * Copyright 2025-2026 Naftiko + * + * 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 io.naftiko.engine.consumes; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.junit.jupiter.api.Test; +import org.restlet.Request; +import org.restlet.data.Method; +import io.naftiko.engine.Resolver; +import io.naftiko.spec.InputParameterSpec; +import io.naftiko.spec.consumes.HttpClientSpec; + +public class HttpClientAdapterHeadersTest { + + @Test + public void setHeadersDoesNotOverrideValueBasedHeader() { + HttpClientSpec spec = new HttpClientSpec("test", "https://api.example.com", null); + + InputParameterSpec notionVersion = new InputParameterSpec(); + notionVersion.setName("Notion-Version"); + notionVersion.setIn("header"); + notionVersion.setValue("2025-09-03"); + spec.getInputParameters().add(notionVersion); + + HttpClientAdapter adapter = new HttpClientAdapter(null, spec); + Request request = new Request(Method.GET, "https://api.example.com/v1/pages"); + + Map params = new ConcurrentHashMap<>(); + Resolver.resolveInputParametersToRequest(request, spec.getInputParameters(), params); + adapter.setHeaders(request); + + String header = request.getHeaders().getFirstValue("Notion-Version", true); + assertNotNull(header, "Notion-Version header should remain present"); + assertEquals("2025-09-03", header, + "Notion-Version should not be overwritten when declared with value"); + } + + @Test + public void resolveInputParametersUsesMustacheInValue() { + Request request = new Request(Method.GET, "https://api.example.com/v1/pages"); + + InputParameterSpec dynamicHeader = new InputParameterSpec(); + dynamicHeader.setName("X-Trace"); + dynamicHeader.setIn("header"); + dynamicHeader.setValue("trace-{{requestId}}"); + + Map params = new ConcurrentHashMap<>(); + params.put("requestId", "abc-123"); + + Resolver.resolveInputParametersToRequest(request, + java.util.List.of(dynamicHeader), params); + + String header = request.getHeaders().getFirstValue("X-Trace", true); + assertEquals("trace-abc-123", header, + "value should support Mustache template resolution"); + } +}