Skip to content

Commit

Permalink
[CDP] Provide an in-process proxy for stubbing responses
Browse files Browse the repository at this point in the history
This makes use of the CDP to provide a lightweight mechanism
for intercepting requests from the browser to the server,
allowing them to be overridden if necessary.

In order to do this, the user needs to be provide a `Route`
from our own HTTP abstractions. If the route matches, then
it is called to figure out what to return to the browser.

This allows tests to stub out backends if they see this as
being necessary.
  • Loading branch information
shs96c committed Aug 1, 2019
1 parent de923a2 commit 62e09d6
Show file tree
Hide file tree
Showing 23 changed files with 730 additions and 17 deletions.
2 changes: 2 additions & 0 deletions java/client/src/org/openqa/selenium/devtools/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ java_library(
"//java/client/src/org/openqa/selenium/chromium:__pkg__",
"//java/client/src/org/openqa/selenium/edge:__pkg__",
"//java/client/src/org/openqa/selenium/remote:__pkg__",
"//java/client/src/org/openqa/selenium/support/devtools:__pkg__",
"//java/client/test/org/openqa/selenium/devtools:__pkg__",
"//java/client/test/org/openqa/selenium/support/devtools:__pkg__",
],
deps = [
"//java/client/src/org/openqa/selenium:core",
Expand Down
19 changes: 19 additions & 0 deletions java/client/src/org/openqa/selenium/devtools/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class Command<X> {
private final String method;
private final Map<String, Object> params;
private final Function<JsonInput, X> mapper;
private final boolean sendsResponse;

public Command(String method, Map<String, Object> params) {
this(method, params, Void.class);
Expand All @@ -42,9 +43,15 @@ public Command(String method, Map<String, Object> params, Type typeOfX) {
}

public Command(String method, Map<String, Object> params, Function<JsonInput, X> mapper) {
this(method, params, mapper, true);
}

private Command(String method, Map<String, Object> params, Function<JsonInput, X> mapper, boolean sendsResponse) {
this.method = Objects.requireNonNull(method, "Method name must be set.");
this.params = ImmutableMap.copyOf(Objects.requireNonNull(params, "Command parameters must be set."));
this.mapper = Objects.requireNonNull(mapper, "Mapper for result must be set.");

this.sendsResponse = sendsResponse;
}

public String getMethod() {
Expand All @@ -58,4 +65,16 @@ public Map<String, Object> getParams() {
Function<JsonInput, X> getMapper() {
return mapper;
}

public boolean getSendsResponse() {
return sendsResponse;
}

/**
* Some CDP commands do not appear to send responses, and so are really hard
* to deal with. Work around that by flagging those commands.
*/
public Command<X> doesNotSendResponse() {
return new Command<>(method, params, mapper, false);
}
}
22 changes: 16 additions & 6 deletions java/client/src/org/openqa/selenium/devtools/Connection.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.logging.Logger;

public class Connection implements Closeable {

private static final Logger LOG = Logger.getLogger(Connection.class.getName());
private static final Json JSON = new Json();
private static final AtomicLong NEXT_ID = new AtomicLong(1L);
private final WebSocket socket;
Expand All @@ -63,10 +65,12 @@ public <X> CompletableFuture<X> send(SessionId sessionId, Command<X> command) {
long id = NEXT_ID.getAndIncrement();

CompletableFuture<X> result = new CompletableFuture<>();
methodCallbacks.put(id, input -> {
X value = command.getMapper().apply(input);
result.complete(value);
});
if (command.getSendsResponse()) {
methodCallbacks.put(id, input -> {
X value = command.getMapper().apply(input);
result.complete(value);
});
}

ImmutableMap.Builder<String, Object> serialized = ImmutableMap.builder();
serialized.put("id", id);
Expand All @@ -76,8 +80,13 @@ public <X> CompletableFuture<X> send(SessionId sessionId, Command<X> command) {
serialized.put("sessionId", sessionId);
}

LOG.info(JSON.toJson(serialized.build()));
socket.sendText(JSON.toJson(serialized.build()));

if (!command.getSendsResponse() ) {
result.complete(null);
}

return result;
}

Expand Down Expand Up @@ -119,6 +128,7 @@ public void onText(CharSequence data) {
// TODO: decode once, and once only

String asString = String.valueOf(data);
LOG.info(asString);

Map<String, Object> raw = JSON.toType(asString, MAP_TYPE);
if (raw.get("id") instanceof Number && raw.get("result") != null) {
Expand All @@ -143,7 +153,7 @@ public void onText(CharSequence data) {
input.endObject();
}
} else if (raw.get("method") instanceof String && raw.get("params") instanceof Map) {
System.out.println("Seen: " + raw);
LOG.fine("Seen: " + raw);

// TODO: Also only decode once.
eventCallbacks.keySet().stream()
Expand Down Expand Up @@ -181,7 +191,7 @@ public void onText(CharSequence data) {
}
});
} else {
System.out.println("Unhandled type: " + data);
LOG.warning("Unhandled type: " + data);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion java/client/src/org/openqa/selenium/devtools/DevTools.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public <X> void addListener(Event<X> event, Consumer<X> handler) {
connection.addListener(event, handler);
}

public void createSessionIfThereIsNoOne() {
public void createSessionIfThereIsNotOne() {
if (cdpSession == null) {
createSession();
}
Expand Down
76 changes: 76 additions & 0 deletions java/client/src/org/openqa/selenium/devtools/fetch/Fetch.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.openqa.selenium.devtools.fetch;

import com.google.common.collect.ImmutableMap;
import org.openqa.selenium.devtools.Command;
import org.openqa.selenium.devtools.Event;
import org.openqa.selenium.devtools.fetch.model.HeaderEntry;
import org.openqa.selenium.devtools.fetch.model.RequestId;
import org.openqa.selenium.devtools.fetch.model.RequestPattern;
import org.openqa.selenium.devtools.fetch.model.RequestPaused;
import org.openqa.selenium.devtools.network.model.ErrorReason;

import java.util.List;
import java.util.Optional;

public class Fetch {

public static Command<Void> disable() {
return new Command<>("Fetch.disable", ImmutableMap.of());
}

public static Command<Void> enable(
Optional<List<RequestPattern>> requestPatterns,
Optional<Boolean> handleAuthRequests) {

ImmutableMap.Builder<String, Object> args = ImmutableMap.builder();
requestPatterns.ifPresent(patterns -> args.put("patterns", patterns));
handleAuthRequests.ifPresent(authRequests -> args.put("handleAuthRequests", authRequests));

return new Command<>("Fetch.enable", args.build());
}

public static Command<Void> failRequest(RequestId requestId, ErrorReason errorReason) {
return new Command<Void>(
"Fetch.failRequest",
ImmutableMap.of("requestId", requestId, "errorReason", errorReason))
.doesNotSendResponse();
}

public static Command<Void> fulfillRequest(
RequestId requestId,
int responseCode,
List<HeaderEntry> responseHeaders,
Optional<String> body,
Optional<String> responsePhrase) {

ImmutableMap.Builder<String, Object> args = ImmutableMap.builder();
args.put("requestId", requestId);
args.put("responseCode", responseCode);
args.put("responseHeaders", responseHeaders);
body.ifPresent(text -> args.put("body", text));
responsePhrase.ifPresent(phrase -> args.put("responsePhrase", phrase));

return new Command<Void>("Fetch.fulfillRequest", args.build()).doesNotSendResponse();
}

public static Command<Void> continueRequest(
RequestId requestId,
Optional<String> url,
Optional<String> method,
Optional<String> postData,
Optional<List<HeaderEntry>> headers) {

ImmutableMap.Builder<String, Object> args = ImmutableMap.builder();
args.put("requestId", requestId);
url.ifPresent(u -> args.put("url", u));
method.ifPresent(m -> args.put("method", m));
postData.ifPresent(data -> args.put("postData", data));
headers.ifPresent(h -> args.put("headers", headers));

return new Command<Void>("Fetch.continueRequest", args.build()).doesNotSendResponse();
}

public static Event<RequestPaused> requestPaused() {
return new Event<>("Fetch.requestPaused", input -> input.read(RequestPaused.class));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.openqa.selenium.devtools.fetch.model;

import com.google.common.collect.ImmutableMap;
import org.openqa.selenium.json.JsonInput;

import java.util.Map;
import java.util.Objects;

public class HeaderEntry {

private final String name;
private final String value;

public HeaderEntry(String name, String value) {
this.name = Objects.requireNonNull(name);
this.value = Objects.requireNonNull(value);
}

public String getName() {
return name;
}

public String getValue() {
return value;
}

private static HeaderEntry fromJson(JsonInput input) {
String name = null;
String value = null;

input.beginObject();
while (input.hasNext()) {
switch (input.nextName()) {
case "name":
name = input.nextString();
break;

case "value":
value = input.nextString();
break;

default:
input.skipValue();
break;
}
}
input.endObject();

return new HeaderEntry(name, value);
}

private Map<String, String> toJson() {
return ImmutableMap.of("name", name, "value", value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.openqa.selenium.devtools.fetch.model;

import java.util.Objects;

public class RequestId {

private final String id;

private RequestId(String id) {
this.id = Objects.requireNonNull(id);
}

private static RequestId fromJson(String id) {
return new RequestId(id);
}

private String toJson() {
return id;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.openqa.selenium.devtools.fetch.model;

import com.google.common.collect.ImmutableMap;
import org.openqa.selenium.devtools.network.model.ResourceType;
import org.openqa.selenium.json.JsonInput;

import java.util.Map;
import java.util.Objects;
import java.util.Optional;

public class RequestPattern {

private final Optional<String> urlPattern;
private final Optional<ResourceType> resourceType;
private final Optional<RequestStage> requestStage;

public RequestPattern(
Optional<String> urlPattern,
Optional<ResourceType> resourceType,
Optional<RequestStage> requestStage) {

this.urlPattern = Objects.requireNonNull(urlPattern);
this.resourceType = Objects.requireNonNull(resourceType);
this.requestStage = Objects.requireNonNull(requestStage);
}

public Optional<String> getUrlPattern() {
return urlPattern;
}

public Optional<ResourceType> getResourceType() {
return resourceType;
}

public Optional<RequestStage> getRequestStage() {
return requestStage;
}

private Map<String, Object> toJson() {
ImmutableMap.Builder<String, Object> blob = ImmutableMap.builder();
urlPattern.ifPresent(pattern -> blob.put("urlPattern", pattern));
resourceType.ifPresent(type -> blob.put("resourceType", type));
requestStage.ifPresent(stage -> blob.put("requestStage", stage));
return blob.build();
}

private static RequestPattern fromJson(JsonInput input) {
Optional<String> urlPattern = Optional.empty();
Optional<ResourceType> resourceType = Optional.empty();
Optional<RequestStage> requestStage = Optional.empty();

input.beginObject();
while (input.hasNext()) {
switch (input.nextName()) {
case "urlPattern":
urlPattern = Optional.of(input.nextString());
break;

case "resourceType":
resourceType = Optional.of(input.read(ResourceType.class));
break;

case "requestStage":
requestStage = Optional.of(input.read(RequestStage.class));
break;

default:
input.skipValue();
break;
}
}
input.endObject();

return new RequestPattern(urlPattern, resourceType, requestStage);
}
}

0 comments on commit 62e09d6

Please sign in to comment.