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 # 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. # even be malformed on purpose, so we do not want to run prettier on them.
test/**/data test/**/data
tools/simple-httpbin/www-files tools/http-test-helper/www-files
# GUI # GUI
**/scala-parser.js **/scala-parser.js

View File

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

View File

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

View File

@ -30,7 +30,7 @@ type Enso_User
response = HTTP.fetch user_api HTTP_Method.Get [auth_header] response = HTTP.fetch user_api HTTP_Method.Get [auth_header]
response.if_not_error <| response.if_not_error <|
js_object = response.decode_as_json js_object = response.decode_as_json
js_object.into Enso_User Enso_User.from js_object
## Lists all known users. ## Lists all known users.
list : Vector Enso_User list : Vector Enso_User
@ -41,7 +41,7 @@ type Enso_User
response.if_not_error <| response.if_not_error <|
js_object = response.decode_as_json js_object = response.decode_as_json
users = js_object.get 'users' [] users = js_object.get 'users' []
users.map (user-> user.into Enso_User) users.map (user-> Enso_User.from user)
## PRIVATE ## 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 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.Pair.Pair
import project.Data.Text.Text import project.Data.Text.Text
import project.Error.Error import project.Error.Error
import project.Network.HTTP.Header.Header
import project.Nothing.Nothing import project.Nothing.Nothing
import project.Runtime.Ref.Ref import project.Runtime.Ref.Ref
import project.System.Environment
import project.System.File.File import project.System.File.File
polyglot java import org.enso.base.enso_cloud.AuthenticationProvider 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 cloud_root_uri = "" + AuthenticationProvider.getAPIRootURI
## PRIVATE ## PRIVATE
Construct the authoization header for the request Construct the authorization header for the request
authorization_header : Pair Text Text authorization_header : Header
authorization_header = authorization_header =
result = AuthenticationProvider.getToken.if_nothing <| token = AuthenticationProvider.getToken.if_nothing <|
cred_file = File.home / ".enso" / "credentials" f = credentials_file
if cred_file.exists.not then Error.throw Not_Logged_In else if f.exists.not then Error.throw Not_Logged_In else
AuthenticationProvider.setToken (cred_file.read_text) AuthenticationProvider.setToken (f.read_text)
Pair.new "Authorization" "Bearer "+result 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 ## PRIVATE
Root address for listing folders 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.Encoding.Encoding
import project.Data.Text.Text import project.Data.Text.Text
import project.Data.Time.Duration.Duration import project.Data.Time.Duration.Duration
import project.Data.Vector.No_Wrap
import project.Data.Vector.Vector import project.Data.Vector.Vector
import project.Error.Error import project.Error.Error
import project.Errors.Common.Forbidden_Operation import project.Errors.Common.Forbidden_Operation
@ -117,7 +118,7 @@ type HTTP
boundary = body_publisher_and_boundary.second boundary = body_publisher_and_boundary.second
boundary_header_list = if boundary.is_nothing then [] else [Header.multipart_form_data boundary] boundary_header_list = if boundary.is_nothing then [] else [Header.multipart_form_data boundary]
all_headers = headers + boundary_header_list 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 _ : Enso_Secret -> EnsoKeyValuePair.ofSecret h.name h.value.id
_ -> EnsoKeyValuePair.ofText h.name h.value _ -> EnsoKeyValuePair.ofText h.name h.value
@ -178,7 +179,7 @@ type HTTP
## PRIVATE ## PRIVATE
parse_headers : Vector (Header | Pair Text Text) -> Vector Header parse_headers : Vector (Header | Pair Text Text) -> Vector Header
parse_headers headers = 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) _ : Vector -> Header.new (h.at 0) (h.at 1)
_ : Pair -> Header.new (h.at 0) (h.at 1) _ : Pair -> Header.new (h.at 0) (h.at 1)
_ : Header -> h _ : Header -> h

View File

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

View File

@ -12,4 +12,11 @@ polyglot java import org.enso.base.Environment_Utils
`System.getenv` Java call remains unchanged. `System.getenv` Java call remains unchanged.
unsafe_with_environment_override : Text -> Text -> Any -> Any unsafe_with_environment_override : Text -> Text -> Any -> Any
unsafe_with_environment_override key value ~action = 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; package org.enso.base;
import java.util.HashMap; import java.util.HashMap;
import java.util.function.Function;
public class Environment_Utils { public class Environment_Utils {
/** Gets the environment variable, including any overrides. */ /** Gets the environment variable, including any overrides. */
@ -14,30 +13,16 @@ public class Environment_Utils {
} }
} }
/** public static void setOverride(String name, String value) {
* Calls `action` with the provided environment variable. overrides.put(name, value);
* }
* <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 public static void removeOverride(String name) {
* get_environment_variable}). overrides.remove(name);
* }
* <p>This is an internal function that should be used very carefully and only for testing.
*/ public static String getOverride(String name) {
public static <T> T with_environment_variable_override( return overrides.get(name);
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);
}
}
} }
private static final HashMap<String, String> overrides = new HashMap<>(); private static final HashMap<String, String> overrides = new HashMap<>();

View File

@ -1,7 +1,9 @@
package org.enso.base.enso_cloud; package org.enso.base.enso_cloud;
import org.enso.base.Environment_Utils;
public class AuthenticationProvider { public class AuthenticationProvider {
private static String token; private static String token = null;
public static String setToken(String token) { public static String setToken(String token) {
AuthenticationProvider.token = token; AuthenticationProvider.token = token;
@ -13,7 +15,7 @@ public class AuthenticationProvider {
} }
public static String getAPIRootURI() { 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; 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 = spec =
## To run this test locally: ## 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/ $ export ENSO_HTTP_TEST_HTTPBIN_URL=http://localhost:8080/
base_url = Environment.get "ENSO_HTTP_TEST_HTTPBIN_URL" base_url = Environment.get "ENSO_HTTP_TEST_HTTPBIN_URL"
base_url_with_slash = base_url.if_not_nothing <| 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 setting the `ENSO_HTTP_TEST_HTTPBIN_URL` environment variable to a value like
`http://localhost:8080`. The URL may contain a trailing slash. `http://localhost:8080`. The URL may contain a trailing slash.
`tools/simple-httpbin` provides a simple implementation of `httpbin` server. See `tools/http-test-helper` provides a simple implementation of `httpbin` server,
its README for instructions on how to run it. 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.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.Header_Spec as Http_Header_Spec
import project.Network.Http.Request_Spec as Http_Request_Spec import project.Network.Http.Request_Spec as Http_Request_Spec
import project.Network.Http_Spec import project.Network.Http_Spec
@ -105,6 +107,7 @@ main = Test_Suite.run_main <|
Http_Header_Spec.spec Http_Header_Spec.spec
Http_Request_Spec.spec Http_Request_Spec.spec
Http_Spec.spec Http_Spec.spec
Enso_Cloud_Spec.spec
Import_Loop_Spec.spec Import_Loop_Spec.spec
Interval_Spec.spec Interval_Spec.spec
Java_Interop_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 = spec =
## To run this test locally: ## 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/ $ export ENSO_HTTP_TEST_HTTPBIN_URL=http://localhost:8080/
base_url = Environment.get "ENSO_HTTP_TEST_HTTPBIN_URL" base_url = Environment.get "ENSO_HTTP_TEST_HTTPBIN_URL"
pending_has_url = if base_url != Nothing then Nothing else 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" <| 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"] 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. How this is interpreted in practice depends on the server.
expected_response = Json.parse <| ''' expected_response = Json.parse <| '''
{ {

View File

@ -7,7 +7,7 @@ import Standard.Test.Extensions
spec = spec =
## To run this test locally: ## 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/ $ export ENSO_HTTP_TEST_HTTPBIN_URL=http://localhost:8080/
base_url = case Environment.get "ENSO_HTTP_TEST_HTTPBIN_URL" of base_url = case Environment.get "ENSO_HTTP_TEST_HTTPBIN_URL" of
Nothing -> Nothing 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" <| 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" 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 <| 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" 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"; private final String password = "my secret password: 1234@#; ść + \uD83D\uDE0E";
@Override @Override
public void doHandle(HttpExchange exchange) throws IOException { protected void doHandle(HttpExchange exchange) throws IOException {
List<String> authHeaders = exchange.getRequestHeaders().get("Authorization"); List<String> authHeaders = exchange.getRequestHeaders().get("Authorization");
if (authHeaders == null || authHeaders.isEmpty()) { if (authHeaders == null || authHeaders.isEmpty()) {
sendResponse(401, "Not authorized.", exchange); sendResponse(401, "Not authorized.", exchange);

View File

@ -5,7 +5,7 @@ import java.io.IOException;
public class CrashingTestHandler extends SimpleHttpHandler { public class CrashingTestHandler extends SimpleHttpHandler {
@Override @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 // This exception will be logged by SimpleHttpHandler, but that's OK - let's know that this
// crash is happening. // crash is happening.
throw new RuntimeException("This handler crashes on purpose."); 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.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.enso.shttp.cloud_mock.CloudRoot;
import sun.misc.Signal; import sun.misc.Signal;
import sun.misc.SignalHandler; import sun.misc.SignalHandler;
public class SimpleHTTPBin { public class HTTPTestHelperServer {
private final HttpServer server; private final HttpServer server;
private final State state; 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); InetSocketAddress address = new InetSocketAddress(hostname, port);
server = HttpServer.create(address, 0); server = HttpServer.create(address, 0);
server.setExecutor(null); server.setExecutor(null);
@ -52,18 +53,18 @@ public class SimpleHTTPBin {
public static void main(String[] args) { public static void main(String[] args) {
if (args.length != 2) { if (args.length != 2) {
System.err.println("Usage: SimpleHTTPBin <host> <port>"); System.err.println("Usage: http-test-helper <host> <port>");
System.exit(1); System.exit(1);
} }
String host = args[0]; String host = args[0];
SimpleHTTPBin server = null; HTTPTestHelperServer server = null;
try { try {
int port = Integer.valueOf(args[1]); int port = Integer.valueOf(args[1]);
server = new SimpleHTTPBin(host, port); server = new HTTPTestHelperServer(host, port);
setupEndpoints(server); setupEndpoints(server);
final SimpleHTTPBin server1 = server; final HTTPTestHelperServer server1 = server;
SignalHandler stopServerHandler = SignalHandler stopServerHandler =
(Signal sig) -> { (Signal sig) -> {
System.out.println("Stopping server... (interrupt)"); 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()) { for (HttpMethod method : HttpMethod.values()) {
String path = "/" + method.toString().toLowerCase(); String path = "/" + method.toString().toLowerCase();
server.addHandler(path, new TestHandler(method)); server.addHandler(path, new TestHandler(method));
@ -108,12 +109,19 @@ public class SimpleHTTPBin {
server.addHandler("/test_token_auth", new TokenAuthTestHandler()); server.addHandler("/test_token_auth", new TokenAuthTestHandler());
server.addHandler("/test_basic_auth", new BasicAuthTestHandler()); server.addHandler("/test_basic_auth", new BasicAuthTestHandler());
server.addHandler("/crash", new CrashingTestHandler()); server.addHandler("/crash", new CrashingTestHandler());
CloudRoot cloudRoot = new CloudRoot();
server.addHandler(cloudRoot.prefix, cloudRoot);
setupFileServer(server); setupFileServer(server);
} }
private static void setupFileServer(SimpleHTTPBin server) throws URISyntaxException { private static void setupFileServer(HTTPTestHelperServer server) throws URISyntaxException {
Path myRuntimeJar = Path myRuntimeJar =
Path.of(SimpleHTTPBin.class.getProtectionDomain().getCodeSource().getLocation().toURI()) Path.of(
HTTPTestHelperServer.class
.getProtectionDomain()
.getCodeSource()
.getLocation()
.toURI())
.toAbsolutePath(); .toAbsolutePath();
Path projectRoot = findProjectRoot(myRuntimeJar); Path projectRoot = findProjectRoot(myRuntimeJar);
Path testFilesRoot = projectRoot.resolve(pathToWWW); 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) { private static boolean looksLikeProjectRoot(Path path) {
return Stream.of("build.sbt", "tools", "project", pathToWWW) 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.io.IOException;
import java.util.List; import java.util.List;
public class TokenAuthTestHandler extends SimpleHttpHandler { public abstract class HandlerWithTokenAuth extends SimpleHttpHandler {
private final String secretToken = "deadbeef-coffee-1234"; protected abstract String getSecretToken();
protected abstract void handleAuthorized(HttpExchange exchange) throws IOException;
@Override @Override
public void doHandle(HttpExchange exchange) throws IOException { protected void doHandle(HttpExchange exchange) throws IOException {
List<String> authHeaders = exchange.getRequestHeaders().get("Authorization"); List<String> authHeaders = exchange.getRequestHeaders().get("Authorization");
if (authHeaders == null || authHeaders.isEmpty()) { if (authHeaders == null || authHeaders.isEmpty()) {
sendResponse(401, "Not authorized.", exchange); sendResponse(401, "Not authorized.", exchange);
@ -26,12 +28,12 @@ public class TokenAuthTestHandler extends SimpleHttpHandler {
} }
String providedToken = authHeader.substring(prefix.length()); String providedToken = authHeader.substring(prefix.length());
boolean authorized = providedToken.equals(secretToken); boolean authorized = providedToken.equals(getSecretToken());
if (!authorized) { if (!authorized) {
sendResponse(403, "Invalid token.", exchange); sendResponse(403, "Invalid token.", exchange);
return; 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 { public class HeaderTestHandler extends SimpleHttpHandler {
@Override @Override
public void doHandle(HttpExchange exchange) throws IOException { protected void doHandle(HttpExchange exchange) throws IOException {
URI uri = exchange.getRequestURI(); URI uri = exchange.getRequestURI();
URIBuilder builder = new URIBuilder(uri); URIBuilder builder = new URIBuilder(uri);
try { 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) protected final void sendResponse(int code, String message, HttpExchange exchange)
throws IOException { throws IOException {

View File

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