Skip to content

Commit

Permalink
Azure HTTP Functions compliant with TCK (#509)
Browse files Browse the repository at this point in the history
  • Loading branch information
timyates authored Jun 30, 2023
1 parent d58c0b4 commit 81e9e18
Show file tree
Hide file tree
Showing 57 changed files with 1,822 additions and 1,384 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2020 original authors
* Copyright 2017-2023 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,6 +20,8 @@
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatusType;
import io.micronaut.azure.function.http.AzureFunctionHttpRequest;
import io.micronaut.azure.function.http.AzureFunctionHttpResponse;
import io.micronaut.azure.function.http.BinaryContentConfiguration;
import io.micronaut.azure.function.http.HttpRequestMessageBuilder;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.env.Environment;
Expand All @@ -29,7 +31,6 @@
import io.micronaut.core.io.socket.SocketUtils;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.context.ServerContextPathProvider;
import io.micronaut.http.server.HttpServerConfiguration;
import io.micronaut.http.server.exceptions.HttpServerException;
import io.micronaut.http.server.exceptions.ServerStartupException;
Expand All @@ -38,19 +39,25 @@
import io.micronaut.servlet.http.BodyBuilder;
import io.micronaut.servlet.http.ServletExchange;
import io.micronaut.servlet.http.ServletHttpHandler;
import io.micronaut.servlet.http.ServletHttpResponse;
import jakarta.inject.Singleton;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler;

import jakarta.inject.Singleton;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.*;
import java.net.BindException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
Expand All @@ -68,27 +75,22 @@
final class AzureFunctionEmbeddedServer implements EmbeddedServer {
private final ApplicationContext applicationContext;
private final boolean randomPort;
private final ServerContextPathProvider contextPathProvider;
private final ConversionService conversionService;
private int port;
private final AtomicBoolean running = new AtomicBoolean(false);
private Server server;
private String contextPath;

/**
* Default cosntructor.
* Default constructor.
* @param applicationContext the app context
* @param httpServerConfiguration the http server configuration
* @param contextPathProvider THe context path provider
*/
AzureFunctionEmbeddedServer(
ApplicationContext applicationContext,
HttpServerConfiguration httpServerConfiguration,
ServerContextPathProvider contextPathProvider,
ConversionService conversionService
) {
this.applicationContext = applicationContext;
this.contextPathProvider = contextPathProvider;
this.conversionService = conversionService;
Optional<Integer> port = httpServerConfiguration.getPort();
if (port.isPresent()) {
Expand Down Expand Up @@ -118,14 +120,9 @@ public EmbeddedServer start() {
try {
this.server = new Server(port);
ContextHandler context = new ContextHandler();
this.contextPath = contextPathProvider.getContextPath();
if (contextPath == null) {
contextPath = "/api";
}
context.setContextPath(contextPath);
context.setResourceBase(".");
context.setClassLoader(Thread.currentThread().getContextClassLoader());
context.setHandler(new AzureHandler(getApplicationContext(), contextPath, conversionService));
context.setHandler(new AzureHandler(getApplicationContext(), conversionService));
server.setHandler(context);
this.server.setHandler(context);
this.server.start();
Expand Down Expand Up @@ -215,15 +212,12 @@ public boolean isRunning() {
private static final class AzureHandler extends AbstractHandler {

private final ServletHttpHandler<HttpRequestMessage<Optional<String>>, HttpResponseMessage> httpHandler;
private final String contextPath;

/**
* Default constructor.
* @param applicationContext The app context
* @param contextPath The context path
*/
AzureHandler(ApplicationContext applicationContext, String contextPath, ConversionService conversionService) {
this.contextPath = contextPath;
AzureHandler(ApplicationContext applicationContext, ConversionService conversionService) {
httpHandler = new ServletHttpHandler<>(applicationContext, conversionService) {
@Override
public boolean isRunning() {
Expand All @@ -243,6 +237,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques
request.getRequestURI(),
httpHandler.getApplicationContext()
);

Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String s = headerNames.nextElement();
Expand All @@ -252,20 +247,19 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques
requestMessageBuilder.header(s, v);
}
}

Enumeration<String> parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()) {
String s = parameterNames.nextElement();
Enumeration<String> headers = request.getHeaders(s);
while (headers.hasMoreElements()) {
String v = headers.nextElement();
requestMessageBuilder.parameter(s, v);
}
String[] parameterValues = request.getParameterValues(s);
requestMessageBuilder.parameter(s, String.join(",", parameterValues));
}


HttpMethod httpMethod = HttpMethod.parse(request.getMethod());
if (HttpMethod.permitsRequestBody(httpMethod)) {
try (BufferedReader requestBody = request.getReader()) {
try (InputStream inputStream = request.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader requestBody = new BufferedReader(inputStreamReader)) {
String body = IOUtils.readText(requestBody);
requestMessageBuilder.body(body);
} catch (IOException e) {
Expand All @@ -274,30 +268,45 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques
}

HttpRequestMessage<Optional<String>> requestMessage = requestMessageBuilder.buildEncoded();
ConversionService handlerConversionService = httpHandler.getApplicationContext().getBean(ConversionService.class);
BinaryContentConfiguration binaryContentConfiguration = httpHandler.getApplicationContext().getBean(BinaryContentConfiguration.class);
AzureFunctionHttpRequest<?> azureFunctionHttpRequest =
new AzureFunctionHttpRequest<>(
contextPath,
requestMessage,
httpHandler.getMediaTypeCodecRegistry(),
new AzureFunctionHttpResponse<>(
requestMessage,
handlerConversionService,
binaryContentConfiguration
),
new DefaultExecutionContext(),
httpHandler.getApplicationContext().getBean(ConversionService.class),
handlerConversionService,
binaryContentConfiguration,
httpHandler.getApplicationContext().getBean(BodyBuilder.class)
);

ServletExchange<HttpRequestMessage<Optional<String>>, HttpResponseMessage> exchange =
httpHandler.exchange(azureFunctionHttpRequest);

HttpResponseMessage httpResponseMessage = exchange.getResponse().getNativeResponse();
ServletHttpResponse<HttpResponseMessage, ?> exchangeResponse = exchange.getResponse();
HttpResponseMessage httpResponseMessage = exchangeResponse.getNativeResponse();
HttpStatusType httpStatus = httpResponseMessage.getStatus();
byte[] bodyAsBytes = (byte[]) httpResponseMessage.getBody();

Object bodyObject = httpResponseMessage.getBody();
byte[] bodyAsBytes = null;
if (bodyObject instanceof CharSequence charBody) {
bodyAsBytes = charBody.toString().getBytes(exchangeResponse.getCharacterEncoding());
} else if (bodyObject instanceof byte[] byteBody) {
bodyAsBytes = byteBody;
}
response.setStatus(httpStatus.value());
final boolean hasBody = bodyAsBytes != null;
response.setContentLength(hasBody ? bodyAsBytes.length : 0);
if (httpResponseMessage instanceof HttpHeaders) {
HttpHeaders headers = (HttpHeaders) httpResponseMessage;
if (httpResponseMessage instanceof HttpHeaders headers) {
headers.forEach((name, values) -> {
for (String value : values) {
response.addHeader(name, value);
if (!response.containsHeader(name)) {
response.addHeader(name, value);
}
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import jakarta.inject.Inject
import static io.micronaut.http.HttpHeaders.*

@MicronautTest
@Property(name = "micronaut.server.context-path", value = "/api")
class AzureFunctionCorsSpec extends Specification implements TestPropertyProvider {

@Inject @Client("/") HttpClient client
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.micronaut.azure.function.http.test

import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpRequest
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
Expand All @@ -14,7 +16,10 @@ import spock.lang.Specification
import jakarta.inject.Inject

@MicronautTest
@Property(name = "spec.name", value = "AzureFunctionEmbeddedServerSpec")
@Property(name = "micronaut.server.context-path", value = "/api")
class AzureFunctionEmbeddedServerSpec extends Specification {

@Inject
@Client('/')
HttpClient client
Expand All @@ -36,6 +41,7 @@ class AzureFunctionEmbeddedServerSpec extends Specification {
result == 'goodbody'
}

@Requires(property = 'spec.name', value = 'AzureFunctionEmbeddedServerSpec')
@Controller('/test')
static class TestController {
@Get(value = '/', produces = MediaType.TEXT_PLAIN)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.micronaut.azure.function.http.test

import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpRequest
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification

@MicronautTest
@Property(name = "spec.name", value = "ContextAzureFunctionEmbeddedServerSpec")
@Property(name = "micronaut.server.context-path", value = "/woo")
class ContextAzureFunctionEmbeddedServerSpec extends Specification {
@Inject
@Client('/')
HttpClient client

void 'test invoke function via server'() {
when:
def result = client.toBlocking().retrieve('/woo/test')

then:
result == 'good'
}

void 'test invoke post via server'() {
when:
def result = client.toBlocking().retrieve(HttpRequest.POST('/woo/test', "body")
.contentType(MediaType.TEXT_PLAIN), String)

then:
result == 'goodbody'
}

@Requires(property = 'spec.name', value = 'ContextAzureFunctionEmbeddedServerSpec')
@Controller('/test')
static class TestController {
@Get(value = '/', produces = MediaType.TEXT_PLAIN)
String test() {
return 'good'
}

@Post(value = '/', processes = MediaType.TEXT_PLAIN)
String test(@Body String body) {
return 'good' + body
}
}
}
6 changes: 6 additions & 0 deletions azure-function-http/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ dependencies {
testCompileOnly(mn.micronaut.inject.groovy)
testAnnotationProcessor(mn.micronaut.inject.java)
}

spotless {
java {
targetExclude("**/io/micronaut/azure/function/http/QueryStringDecoder.java")
}
}

This file was deleted.

Loading

0 comments on commit 81e9e18

Please sign in to comment.