mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 18:01:38 +03:00
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:
parent
aa05389f4a
commit
724f8d2a56
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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()?;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -97,6 +97,7 @@ type Header
|
||||
|
||||
Arguments:
|
||||
- token: The token.
|
||||
authorization_bearer : Text -> Header
|
||||
authorization_bearer token:Text =
|
||||
Header.authorization ("Bearer " + token)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<>();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 <|
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
117
test/Tests/src/Network/Enso_Cloud/Enso_Cloud_Spec.enso
Normal file
117
test/Tests/src/Network/Enso_Cloud/Enso_Cloud_Spec.enso
Normal 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
|
@ -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 <| '''
|
||||
{
|
||||
|
@ -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"
|
||||
|
||||
|
19
tools/http-test-helper/README.md
Normal file
19
tools/http-test-helper/README.md
Normal 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
|
||||
```
|
@ -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);
|
@ -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.");
|
@ -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)
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 {
|
@ -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 {
|
@ -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";
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
""";
|
||||
}
|
@ -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
|
||||
```
|
Loading…
Reference in New Issue
Block a user