Add tests for Enso Cloud auth + simple API mock for Enso_User (#8511)

- Closes #8354
- Extends `simple-httpbin` with a simple mock of the Cloud API (currently it checks the token and serves the `/users` endpoint).
- Renames `simple-httpbin` to `http-test-helper`.
This commit is contained in:
Radosław Waśko 2023-12-19 18:41:09 +01:00 committed by GitHub
parent aa05389f4a
commit 724f8d2a56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 383 additions and 90 deletions

View File

@ -21,7 +21,7 @@ resources/python
# The files in the `data` directory of our tests may have specific structure or
# even be malformed on purpose, so we do not want to run prettier on them.
test/**/data
tools/simple-httpbin/www-files
tools/http-test-helper/www-files
# GUI
**/scala-parser.js

View File

@ -318,7 +318,7 @@ lazy val enso = (project in file("."))
`std-image`,
`std-table`,
`std-aws`,
`simple-httpbin`,
`http-test-helper`,
`enso-test-java-helpers`,
`exploratory-benchmark-java-helpers`,
`benchmark-java-helpers`,
@ -2909,13 +2909,13 @@ val stdBitsProjects =
val allStdBits: Parser[String] =
stdBitsProjects.map(v => v: Parser[String]).reduce(_ | _)
lazy val `simple-httpbin` = project
.in(file("tools") / "simple-httpbin")
lazy val `http-test-helper` = project
.in(file("tools") / "http-test-helper")
.settings(
customFrgaalJavaCompilerSettings(targetJdk = "21"),
autoScalaLibrary := false,
Compile / javacOptions ++= Seq("-Xlint:all"),
Compile / run / mainClass := Some("org.enso.shttp.SimpleHTTPBin"),
Compile / run / mainClass := Some("org.enso.shttp.HTTPTestHelperServer"),
assembly / mainClass := (Compile / run / mainClass).value,
libraryDependencies ++= Seq(
"org.apache.commons" % "commons-text" % commonsTextVersion,

View File

@ -29,7 +29,7 @@ pub async fn get_and_spawn_httpbin(
) -> Result<Spawned> {
let process = sbt
.command()?
.arg(format!("simple-httpbin/run localhost {port}"))
.arg(format!("http-test-helper/run localhost {port}"))
.kill_on_drop(true)
.spawn()?;

View File

@ -30,7 +30,7 @@ type Enso_User
response = HTTP.fetch user_api HTTP_Method.Get [auth_header]
response.if_not_error <|
js_object = response.decode_as_json
js_object.into Enso_User
Enso_User.from js_object
## Lists all known users.
list : Vector Enso_User
@ -41,7 +41,7 @@ type Enso_User
response.if_not_error <|
js_object = response.decode_as_json
users = js_object.get 'users' []
users.map (user-> user.into Enso_User)
users.map (user-> Enso_User.from user)
## PRIVATE
Enso_User.from (that:JS_Object) = if ["name", "email", "id"].any (k-> that.contains_key k . not) then Error.throw (Illegal_Argument.Error "Invalid JSON for an Enso_User.") else

View File

@ -1,8 +1,10 @@
import project.Data.Pair.Pair
import project.Data.Text.Text
import project.Error.Error
import project.Network.HTTP.Header.Header
import project.Nothing.Nothing
import project.Runtime.Ref.Ref
import project.System.Environment
import project.System.File.File
polyglot java import org.enso.base.enso_cloud.AuthenticationProvider
@ -11,14 +13,20 @@ polyglot java import org.enso.base.enso_cloud.AuthenticationProvider
cloud_root_uri = "" + AuthenticationProvider.getAPIRootURI
## PRIVATE
Construct the authoization header for the request
authorization_header : Pair Text Text
Construct the authorization header for the request
authorization_header : Header
authorization_header =
result = AuthenticationProvider.getToken.if_nothing <|
cred_file = File.home / ".enso" / "credentials"
if cred_file.exists.not then Error.throw Not_Logged_In else
AuthenticationProvider.setToken (cred_file.read_text)
Pair.new "Authorization" "Bearer "+result
token = AuthenticationProvider.getToken.if_nothing <|
f = credentials_file
if f.exists.not then Error.throw Not_Logged_In else
AuthenticationProvider.setToken (f.read_text)
Header.authorization_bearer token
## PRIVATE
credentials_file : File
credentials_file = case Environment.get "ENSO_CLOUD_CREDENTIALS_FILE" of
Nothing -> File.home / ".enso" / "credentials"
path -> File.new path
## PRIVATE
Root address for listing folders

View File

@ -6,6 +6,7 @@ import project.Data.Set.Set
import project.Data.Text.Encoding.Encoding
import project.Data.Text.Text
import project.Data.Time.Duration.Duration
import project.Data.Vector.No_Wrap
import project.Data.Vector.Vector
import project.Error.Error
import project.Errors.Common.Forbidden_Operation
@ -117,7 +118,7 @@ type HTTP
boundary = body_publisher_and_boundary.second
boundary_header_list = if boundary.is_nothing then [] else [Header.multipart_form_data boundary]
all_headers = headers + boundary_header_list
mapped_headers = all_headers.map h-> case h.value of
mapped_headers = all_headers.map on_problems=No_Wrap.Value h-> case h.value of
_ : Enso_Secret -> EnsoKeyValuePair.ofSecret h.name h.value.id
_ -> EnsoKeyValuePair.ofText h.name h.value
@ -178,7 +179,7 @@ type HTTP
## PRIVATE
parse_headers : Vector (Header | Pair Text Text) -> Vector Header
parse_headers headers =
headers . map h-> case h of
headers . map on_problems=No_Wrap.Value h-> case h of
_ : Vector -> Header.new (h.at 0) (h.at 1)
_ : Pair -> Header.new (h.at 0) (h.at 1)
_ : Header -> h

View File

@ -97,6 +97,7 @@ type Header
Arguments:
- token: The token.
authorization_bearer : Text -> Header
authorization_bearer token:Text =
Header.authorization ("Bearer " + token)

View File

@ -12,4 +12,11 @@ polyglot java import org.enso.base.Environment_Utils
`System.getenv` Java call remains unchanged.
unsafe_with_environment_override : Text -> Text -> Any -> Any
unsafe_with_environment_override key value ~action =
Environment_Utils.with_environment_variable_override key value (_->action)
## This has to be done in Enso, not in Java, due to the bug: https://github.com/enso-org/enso/issues/7117
If done in Java, Enso test functions do not work correctly, because they cannot access State.
old_value = Environment_Utils.getOverride key
restore_previous =
if old_value.is_nothing then Environment_Utils.removeOverride key else Environment_Utils.setOverride key old_value
Panic.with_finalizer restore_previous <|
Environment_Utils.setOverride key value
action

View File

@ -1,7 +1,6 @@
package org.enso.base;
import java.util.HashMap;
import java.util.function.Function;
public class Environment_Utils {
/** Gets the environment variable, including any overrides. */
@ -14,30 +13,16 @@ public class Environment_Utils {
}
}
/**
* Calls `action` with the provided environment variable.
*
* <p>The override is not persisted (its only visible from within the action called by this
* method) and it is only visible by the Enso `Environment.get` method (backed by {@code
* get_environment_variable}).
*
* <p>This is an internal function that should be used very carefully and only for testing.
*/
public static <T> T with_environment_variable_override(
String name, String value, Function<Object, T> action) {
String oldValue = overrides.put(name, value);
boolean was_set = oldValue != null;
try {
// Giving 0 here as an argument, as using null would lead to incorrect behaviour, due to some
// weird Truffle peculiarity.
return action.apply(0);
} finally {
if (was_set) {
overrides.put(name, oldValue);
} else {
overrides.remove(name);
}
}
public static void setOverride(String name, String value) {
overrides.put(name, value);
}
public static void removeOverride(String name) {
overrides.remove(name);
}
public static String getOverride(String name) {
return overrides.get(name);
}
private static final HashMap<String, String> overrides = new HashMap<>();

View File

@ -1,7 +1,9 @@
package org.enso.base.enso_cloud;
import org.enso.base.Environment_Utils;
public class AuthenticationProvider {
private static String token;
private static String token = null;
public static String setToken(String token) {
AuthenticationProvider.token = token;
@ -13,7 +15,7 @@ public class AuthenticationProvider {
}
public static String getAPIRootURI() {
var envUri = System.getenv("ENSO_CLOUD_API_URI");
var envUri = Environment_Utils.get_environment_variable("ENSO_CLOUD_API_URI");
return envUri == null ? "https://7aqkn3tnbc.execute-api.eu-west-1.amazonaws.com/" : envUri;
}

View File

@ -14,7 +14,7 @@ main = Test_Suite.run_main spec
spec =
## To run this test locally:
$ sbt 'simple-httpbin/run localhost 8080'
$ sbt 'http-test-helper/run localhost 8080'
$ export ENSO_HTTP_TEST_HTTPBIN_URL=http://localhost:8080/
base_url = Environment.get "ENSO_HTTP_TEST_HTTPBIN_URL"
base_url_with_slash = base_url.if_not_nothing <|

View File

@ -5,5 +5,6 @@ the localhost. If it is present, the port it listens to should be provided by
setting the `ENSO_HTTP_TEST_HTTPBIN_URL` environment variable to a value like
`http://localhost:8080`. The URL may contain a trailing slash.
`tools/simple-httpbin` provides a simple implementation of `httpbin` server. See
its README for instructions on how to run it.
`tools/http-test-helper` provides a simple implementation of `httpbin` server,
extended with some mock APIs allowing testing basic Enso Cloud functionality.
See its README for instructions on how to run it.

View File

@ -59,6 +59,8 @@ import project.Data.XML.XML_Spec
import project.Data.Vector.Slicing_Helpers_Spec
import project.Network.Enso_Cloud.Enso_Cloud_Spec
import project.Network.Http.Header_Spec as Http_Header_Spec
import project.Network.Http.Request_Spec as Http_Request_Spec
import project.Network.Http_Spec
@ -105,6 +107,7 @@ main = Test_Suite.run_main <|
Http_Header_Spec.spec
Http_Request_Spec.spec
Http_Spec.spec
Enso_Cloud_Spec.spec
Import_Loop_Spec.spec
Interval_Spec.spec
Java_Interop_Spec.spec

View File

@ -0,0 +1,117 @@
from Standard.Base import all
import Standard.Base.Data.Enso_Cloud.Utils as Cloud_Utils
import Standard.Base.Errors.Common.No_Such_Conversion
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import Standard.Base.Network.HTTP.HTTP_Error.HTTP_Error
from Standard.Test import Test, Test_Suite
import Standard.Test.Test_Environment
import Standard.Test.Extensions
polyglot java import org.enso.base.enso_cloud.AuthenticationProvider
## Resets the user token, to avoid cached token from other tests interfering.
reset_token =
AuthenticationProvider.setToken Nothing
spec =
## To run this test locally:
$ sbt 'http-test-helper/run localhost 8080'
$ export ENSO_HTTP_TEST_HTTPBIN_URL=http://localhost:8080/
base_url = Environment.get "ENSO_HTTP_TEST_HTTPBIN_URL"
pending_has_url = if base_url != Nothing then Nothing else
"The Cloud mock tests only run when the `ENSO_HTTP_TEST_HTTPBIN_URL` environment variable is set to URL of the http-test-helper server"
enso_cloud_url = base_url.if_not_nothing <|
with_slash = if base_url.ends_with "/" then base_url else base_url + "/"
with_slash + "enso-cloud-mock/"
tmp_cred_file = File.create_temporary_file "enso-test-credentials" ".txt"
test_token = "TEST-ENSO-TOKEN-caffee"
test_token.write tmp_cred_file
## This helper method is needed, because of the bug https://github.com/enso-org/enso/issues/7117
If the bug is fixed, we could move the overrides to the top-level and not have to re-initialize them.
with_mock_environment ~action =
Test_Environment.unsafe_with_environment_override "ENSO_CLOUD_API_URI" enso_cloud_url <|
Test_Environment.unsafe_with_environment_override "ENSO_CLOUD_CREDENTIALS_FILE" tmp_cred_file.absolute.normalize.path <|
action
with_mock_environment <|
reset_token
Test.group "Enso Cloud Basic Utils" pending=pending_has_url <|
Test.specify "will report Not_Logged_In if no credentials file is found" <|
non_existent_file = (enso_project.data / "nonexistent-file") . absolute . normalize
non_existent_file.exists.should_be_false
Test_Environment.unsafe_with_environment_override "ENSO_CLOUD_CREDENTIALS_FILE" non_existent_file.path <|
# This test has to run before any other Cloud access, otherwise the token may already be cached.
Cloud_Utils.authorization_header.should_fail_with Cloud_Utils.Not_Logged_In
Test.specify "should be able to get the cloud URL from environment" <|
api_url = Cloud_Utils.cloud_root_uri
api_url.should_equal enso_cloud_url
Test.specify "should be able to read the authorization token" <|
Cloud_Utils.authorization_header.to_display_text.should_equal "Authorization: Bearer "+test_token
Test.group "Enso_User" <|
Test.specify "is correctly parsed from JSON" <|
json = Json.parse """
{
"id": "organization-27xJM00p8jWoL2qByTo6tQfciWC",
"name": "Parsed user",
"email": "enso-parse-test@example.com",
"isEnabled": true,
"rootDirectoryId": "directory-27xJM00p8jWoL2qByTo6tQfciWC"
}
parsed_user = Enso_User.from json
parsed_user.id.should_equal "organization-27xJM00p8jWoL2qByTo6tQfciWC"
parsed_user.name.should_equal "Parsed user"
parsed_user.email.should_equal "enso-parse-test@example.com"
parsed_user.is_enabled.should_be_true
# TODO separate Enso_File tests could test that this is a valid directory
home = parsed_user.home
home.is_directory.should_be_true
invalid_json = Json.parse "{}"
Enso_User.from invalid_json . should_fail_with Illegal_Argument
Test.expect_panic No_Such_Conversion (Enso_User.from (Json.parse "[]"))
# These tests should be kept in sync with tools/http-test-helper/src/main/java/org/enso/shttp/?
Test.specify "current user can be fetched from mock API" <|
current = Enso_User.current
current.id.should_equal "organization-27xJM00p8jWoL2qByTo6tQfciWC"
current.name.should_equal "My test User 1"
current.email.should_equal "enso-test-user-1@example.com"
current.is_enabled.should_be_true
Test.specify "user list can be fetched from mock API" <|
users = Enso_User.list
users.length.should_equal 2
users.at 0 . name . should_equal "My test User 1"
users.at 1 . name . should_equal "My test User 2"
users.at 1 . is_enabled . should_be_false
Test.specify "will fail if the user is not logged in" <|
non_existent_file = (enso_project.data / "nonexistent-file") . absolute . normalize
non_existent_file.exists.should_be_false
r = Test_Environment.unsafe_with_environment_override "ENSO_CLOUD_CREDENTIALS_FILE" non_existent_file.path <|
reset_token
Enso_User.current
r.should_fail_with Cloud_Utils.Not_Logged_In
Test.specify "will fail if the token is invalid" <|
invalid_token_file = File.create_temporary_file "enso-test-credentials" "-invalid.txt"
"invalid-token".write invalid_token_file . should_succeed
reset_token
r = Test_Environment.unsafe_with_environment_override "ENSO_CLOUD_CREDENTIALS_FILE" invalid_token_file.absolute.normalize.path <|
Enso_User.current
r.should_fail_with HTTP_Error
r.catch.should_be_a HTTP_Error.Status_Error
r.catch.status_code.code . should_equal 403
# Ensure the token is reset after the last test, so that any other tests will again use the correct one.
reset_token
main = Test_Suite.run_main spec

View File

@ -27,7 +27,7 @@ type Bad_To_Json
spec =
## To run this test locally:
$ sbt 'simple-httpbin/run localhost 8080'
$ sbt 'http-test-helper/run localhost 8080'
$ export ENSO_HTTP_TEST_HTTPBIN_URL=http://localhost:8080/
base_url = Environment.get "ENSO_HTTP_TEST_HTTPBIN_URL"
pending_has_url = if base_url != Nothing then Nothing else
@ -452,7 +452,7 @@ spec =
Test.specify "Multiple content types in the header list are respected" <|
response = Data.post url_post (Request_Body.Text '{"a": "asdf", "b": 123}') headers=[Header.content_type "application/json", Header.content_type "text/plain"]
## Our simple-httpbin server gets 2 Content-Type headers and merges them in the response.
## Our http-test-helper gets 2 Content-Type headers and merges them in the response.
How this is interpreted in practice depends on the server.
expected_response = Json.parse <| '''
{

View File

@ -7,7 +7,7 @@ import Standard.Test.Extensions
spec =
## To run this test locally:
$ sbt 'simple-httpbin/run localhost 8080'
$ sbt 'http-test-helper/run localhost 8080'
$ export ENSO_HTTP_TEST_HTTPBIN_URL=http://localhost:8080/
base_url = case Environment.get "ENSO_HTTP_TEST_HTTPBIN_URL" of
Nothing -> Nothing
@ -129,7 +129,7 @@ spec =
Test.specify "will not convert back to URI if secrets are present in the query arguments" pending="TODO testing secrets is for later" <|
Error.throw "TODO: secrets tests"
# We rely on the simple-httpbin server for these tests, to ensure that the encoding is indeed correctly interpreted by a real-life server:
# We rely on the http-test-helper for these tests, to ensure that the encoding is indeed correctly interpreted by a real-life server:
Test.specify "should correctly handle various characters within the key and value of arguments" pending=pending_has_url <|
base_uri = URI.parse base_url+"get"

View File

@ -0,0 +1,19 @@
# HTTP Test Helper
A simple HTTP Request/Response clone of [httpbin](http://httpbin.org) for
testing purposes, extended with additional functionality allowing for testing
Enso Cloud features.
## Usage
It can be compiled like any other SBT project i.e.
```
sbt> http-test-helper/compile
```
To run, simply invoke the `main` method with the appropriate hostname and port:
```
sbt> http-test-helper/run localhost 8080
```

View File

@ -12,7 +12,7 @@ public class BasicAuthTestHandler extends SimpleHttpHandler {
private final String password = "my secret password: 1234@#; ść + \uD83D\uDE0E";
@Override
public void doHandle(HttpExchange exchange) throws IOException {
protected void doHandle(HttpExchange exchange) throws IOException {
List<String> authHeaders = exchange.getRequestHeaders().get("Authorization");
if (authHeaders == null || authHeaders.isEmpty()) {
sendResponse(401, "Not authorized.", exchange);

View File

@ -5,7 +5,7 @@ import java.io.IOException;
public class CrashingTestHandler extends SimpleHttpHandler {
@Override
public void doHandle(HttpExchange exchange) throws IOException {
protected void doHandle(HttpExchange exchange) throws IOException {
// This exception will be logged by SimpleHttpHandler, but that's OK - let's know that this
// crash is happening.
throw new RuntimeException("This handler crashes on purpose.");

View File

@ -10,15 +10,16 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import org.enso.shttp.cloud_mock.CloudRoot;
import sun.misc.Signal;
import sun.misc.SignalHandler;
public class SimpleHTTPBin {
public class HTTPTestHelperServer {
private final HttpServer server;
private final State state;
public SimpleHTTPBin(String hostname, int port) throws IOException {
public HTTPTestHelperServer(String hostname, int port) throws IOException {
InetSocketAddress address = new InetSocketAddress(hostname, port);
server = HttpServer.create(address, 0);
server.setExecutor(null);
@ -52,18 +53,18 @@ public class SimpleHTTPBin {
public static void main(String[] args) {
if (args.length != 2) {
System.err.println("Usage: SimpleHTTPBin <host> <port>");
System.err.println("Usage: http-test-helper <host> <port>");
System.exit(1);
}
String host = args[0];
SimpleHTTPBin server = null;
HTTPTestHelperServer server = null;
try {
int port = Integer.valueOf(args[1]);
server = new SimpleHTTPBin(host, port);
server = new HTTPTestHelperServer(host, port);
setupEndpoints(server);
final SimpleHTTPBin server1 = server;
final HTTPTestHelperServer server1 = server;
SignalHandler stopServerHandler =
(Signal sig) -> {
System.out.println("Stopping server... (interrupt)");
@ -98,7 +99,7 @@ public class SimpleHTTPBin {
}
}
private static void setupEndpoints(SimpleHTTPBin server) throws URISyntaxException {
private static void setupEndpoints(HTTPTestHelperServer server) throws URISyntaxException {
for (HttpMethod method : HttpMethod.values()) {
String path = "/" + method.toString().toLowerCase();
server.addHandler(path, new TestHandler(method));
@ -108,12 +109,19 @@ public class SimpleHTTPBin {
server.addHandler("/test_token_auth", new TokenAuthTestHandler());
server.addHandler("/test_basic_auth", new BasicAuthTestHandler());
server.addHandler("/crash", new CrashingTestHandler());
CloudRoot cloudRoot = new CloudRoot();
server.addHandler(cloudRoot.prefix, cloudRoot);
setupFileServer(server);
}
private static void setupFileServer(SimpleHTTPBin server) throws URISyntaxException {
private static void setupFileServer(HTTPTestHelperServer server) throws URISyntaxException {
Path myRuntimeJar =
Path.of(SimpleHTTPBin.class.getProtectionDomain().getCodeSource().getLocation().toURI())
Path.of(
HTTPTestHelperServer.class
.getProtectionDomain()
.getCodeSource()
.getLocation()
.toURI())
.toAbsolutePath();
Path projectRoot = findProjectRoot(myRuntimeJar);
Path testFilesRoot = projectRoot.resolve(pathToWWW);
@ -134,7 +142,7 @@ public class SimpleHTTPBin {
}
}
private static final String pathToWWW = "tools/simple-httpbin/www-files";
private static final String pathToWWW = "tools/http-test-helper/www-files";
private static boolean looksLikeProjectRoot(Path path) {
return Stream.of("build.sbt", "tools", "project", pathToWWW)

View File

@ -4,11 +4,13 @@ import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.util.List;
public class TokenAuthTestHandler extends SimpleHttpHandler {
private final String secretToken = "deadbeef-coffee-1234";
public abstract class HandlerWithTokenAuth extends SimpleHttpHandler {
protected abstract String getSecretToken();
protected abstract void handleAuthorized(HttpExchange exchange) throws IOException;
@Override
public void doHandle(HttpExchange exchange) throws IOException {
protected void doHandle(HttpExchange exchange) throws IOException {
List<String> authHeaders = exchange.getRequestHeaders().get("Authorization");
if (authHeaders == null || authHeaders.isEmpty()) {
sendResponse(401, "Not authorized.", exchange);
@ -26,12 +28,12 @@ public class TokenAuthTestHandler extends SimpleHttpHandler {
}
String providedToken = authHeader.substring(prefix.length());
boolean authorized = providedToken.equals(secretToken);
boolean authorized = providedToken.equals(getSecretToken());
if (!authorized) {
sendResponse(403, "Invalid token.", exchange);
return;
}
sendResponse(200, "Authorization successful.", exchange);
handleAuthorized(exchange);
}
}

View File

@ -7,7 +7,7 @@ import org.apache.http.client.utils.URIBuilder;
public class HeaderTestHandler extends SimpleHttpHandler {
@Override
public void doHandle(HttpExchange exchange) throws IOException {
protected void doHandle(HttpExchange exchange) throws IOException {
URI uri = exchange.getRequestURI();
URIBuilder builder = new URIBuilder(uri);
try {

View File

@ -28,7 +28,7 @@ public abstract class SimpleHttpHandler implements HttpHandler {
}
}
public abstract void doHandle(HttpExchange exchange) throws IOException;
protected abstract void doHandle(HttpExchange exchange) throws IOException;
protected final void sendResponse(int code, String message, HttpExchange exchange)
throws IOException {

View File

@ -27,7 +27,7 @@ public class TestHandler extends SimpleHttpHandler {
this.expectedMethod = expectedMethod;
}
public void doHandle(HttpExchange exchange) throws IOException {
protected void doHandle(HttpExchange exchange) throws IOException {
boolean first = true;
String contentType = null;
String textEncoding = "UTF-8";

View File

@ -0,0 +1,17 @@
package org.enso.shttp;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
public class TokenAuthTestHandler extends HandlerWithTokenAuth {
@Override
protected String getSecretToken() {
return "deadbeef-coffee-1234";
}
@Override
protected void handleAuthorized(HttpExchange exchange) throws IOException {
sendResponse(200, "Authorization successful.", exchange);
}
}

View File

@ -0,0 +1,18 @@
package org.enso.shttp.cloud_mock;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
public interface CloudHandler {
boolean canHandle(String subPath);
void handleCloudAPI(CloudExchange exchange) throws IOException;
interface CloudExchange {
HttpExchange getHttpExchange();
String subPath();
void sendResponse(int code, String response) throws IOException;
}
}

View File

@ -0,0 +1,57 @@
package org.enso.shttp.cloud_mock;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.net.URI;
import org.enso.shttp.HandlerWithTokenAuth;
public class CloudRoot extends HandlerWithTokenAuth {
public final String prefix = "/enso-cloud-mock/";
@Override
protected String getSecretToken() {
return "TEST-ENSO-TOKEN-caffee";
}
private final CloudHandler[] handlers = new CloudHandler[] {new UsersHandler()};
@Override
protected void handleAuthorized(HttpExchange exchange) throws IOException {
URI uri = exchange.getRequestURI();
String path = uri.getPath();
int prefixStart = path.indexOf(prefix);
if (prefixStart == -1) {
sendResponse(400, "Invalid URI.", exchange);
return;
}
String subPath = path.substring(prefixStart + prefix.length());
for (CloudHandler handler : handlers) {
if (handler.canHandle(subPath)) {
handler.handleCloudAPI(wrapExchange(subPath, exchange));
return;
}
}
sendResponse(404, "No handler found for: " + subPath, exchange);
}
private CloudHandler.CloudExchange wrapExchange(String subPath, HttpExchange exchange) {
return new CloudHandler.CloudExchange() {
@Override
public HttpExchange getHttpExchange() {
return exchange;
}
@Override
public String subPath() {
return subPath;
}
@Override
public void sendResponse(int code, String response) throws IOException {
CloudRoot.this.sendResponse(code, response, exchange);
}
};
}
}

View File

@ -0,0 +1,65 @@
package org.enso.shttp.cloud_mock;
import java.io.IOException;
public class UsersHandler implements CloudHandler {
private static final String USERS = "users";
@Override
public boolean canHandle(String subPath) {
return subPath.startsWith(USERS);
}
@Override
public void handleCloudAPI(CloudExchange exchange) throws IOException {
String part = exchange.subPath().substring(USERS.length());
switch (part) {
case "/me" -> sendCurrentUser(exchange);
case "" -> sendUserList(exchange);
default -> {
exchange.sendResponse(404, "No handler found for: " + part);
}
}
}
private void sendCurrentUser(CloudExchange exchange) throws IOException {
exchange.sendResponse(200, currentUser);
}
private void sendUserList(CloudExchange exchange) throws IOException {
String response =
"""
{
"users": [
%s,
%s
]
}
"""
.formatted(currentUser, otherUser);
exchange.sendResponse(200, response);
}
private final String currentUser =
"""
{
"id": "organization-27xJM00p8jWoL2qByTo6tQfciWC",
"name": "My test User 1",
"email": "enso-test-user-1@example.com",
"isEnabled": true,
"rootDirectoryId": "directory-27xJM00p8jWoL2qByTo6tQfciWC"
}
""";
private final String otherUser =
"""
{
"id": "organization-44AAA00A8AAAA2AAAAA6AAAAAAA",
"name": "My test User 2",
"email": "enso-test-user-2@example.com",
"isEnabled": false,
"rootDirectoryId": "directory-44AAA00A8AAAA2AAAAA6AAAAAAA"
}
""";
}

View File

@ -1,18 +0,0 @@
# Simple HTTPBin
A simple HTTP Request/Response clone of [httpbin](http://httpbin.org) for
testing purposes.
## Usage
Simple HTTPBin can be compiled like any other SBT project i.e.
```
sbt> simple-httpbin/compile
```
To run, simply invoke the `main` method with the appropriate hostname and port:
```
sbt> simple-httpbin/run localhost 8080
```