Skip to content

Feat/response status text (#5, #6) #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions karate-core/src/main/java/com/intuit/karate/Actions.java
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ public interface Actions {

void status(int status);

void statusText(String text);

void table(String name, List<Map<String, String>> table);

void text(String name, String exp);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,12 @@ public void status(int status) {
engine.status(status);
}

@Override
@When("^status text (.+)")
public void statusText(String text) {
engine.statusText(text);
}

@Override
@When("^match (.+)(=|contains|any|only|deep)(.*)")
public void match(String exp, String op1, String op2, String rhs) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,20 +188,22 @@ public synchronized Response handle(Request req) { // note the [synchronized]
Scenario scenario = fs.getScenario();
if (isMatchingScenario(scenario, engine)) {
Map<String, Object> configureHeaders;
Variable response, responseStatus, responseHeaders, responseDelay;
Variable response, responseStatus, responseStatusText, responseHeaders, responseDelay;
ScenarioActions actions = new ScenarioActions(engine);
Result result = executeScenarioSteps(feature, runtime, scenario, actions);
engine.mockAfterScenario();
configureHeaders = engine.mockConfigureHeaders();
response = engine.vars.remove(ScenarioEngine.RESPONSE);
responseStatus = engine.vars.remove(ScenarioEngine.RESPONSE_STATUS);
responseStatusText = engine.vars.remove(ScenarioEngine.RESPONSE_STATUS_TEXT);
responseHeaders = engine.vars.remove(ScenarioEngine.RESPONSE_HEADERS);
responseDelay = engine.vars.remove(RESPONSE_DELAY);
globals.putAll(engine.shallowCloneVariables());
Response res = new Response(200);
Response res = new Response(200, "OK");
if (result.isFailed()) {
response = new Variable(result.getError().getMessage());
responseStatus = new Variable(500);
responseStatusText = new Variable("Internal Server Error");
} else {
if (corsEnabled) {
res.setHeader("Access-Control-Allow-Origin", "*");
Expand All @@ -226,6 +228,9 @@ public synchronized Response handle(Request req) { // note the [synchronized]
if (responseStatus != null) {
res.setStatus(responseStatus.getAsInt());
}
if (responseStatusText != null) {
res.setStatusText(responseStatusText.getAsString());
}
if (prevEngine != null) {
ScenarioEngine.set(prevEngine);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public class ScenarioEngine {
public static final String RESPONSE = "response";
public static final String RESPONSE_HEADERS = "responseHeaders";
public static final String RESPONSE_STATUS = "responseStatus";
public static final String RESPONSE_STATUS_TEXT = "responseStatusText";
private static final String RESPONSE_BYTES = "responseBytes";
private static final String RESPONSE_COOKIES = "responseCookies";
private static final String RESPONSE_TIME = "responseTime";
Expand Down Expand Up @@ -650,6 +651,7 @@ private void httpInvokeOnce() {
setHiddenVariable(REQUEST_TIME_STAMP, startTime);
setVariable(RESPONSE_TIME, responseTime);
setVariable(RESPONSE_STATUS, response.getStatus());
setVariable(RESPONSE_STATUS_TEXT, response.getStatusText());
setVariable(RESPONSE, body);
if (config.isLowerCaseResponseHeaders()) {
setVariable(RESPONSE_HEADERS, response.getHeadersWithLowerCaseNames());
Expand Down Expand Up @@ -718,6 +720,13 @@ public void status(int status) {
}
}

public void statusText(String text) {
if (!text.equals(response.getStatusText())) {
String message = HttpLogger.getStatusTextFailureMessage(text, config, httpRequest, response);
setFailedReason(new KarateException(message));
}
}

public KeyStore getKeyStore(String trustStoreFile, String password, String type) {
if (trustStoreFile == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ public Response invoke(HttpRequest request) {
}
}
int statusCode = httpResponse.getStatusLine().getStatusCode();
String statusMessage = httpResponse.getStatusLine().getReasonPhrase();
Map<String, List<String>> headers = toHeaders(httpResponse);
List<Cookie> storedCookies = cookieStore.getCookies();
Header[] requestCookieHeaders = httpResponse.getHeaders(HttpConstants.HDR_SET_COOKIE);
Expand Down Expand Up @@ -341,7 +342,7 @@ public Response invoke(HttpRequest request) {
}
headers.put(HttpConstants.HDR_SET_COOKIE, mergedCookieValues);
cookieStore.clear();
Response response = new Response(statusCode, headers, bytes);
Response response = new Response(statusCode, statusMessage, headers, bytes);
httpLogger.logResponse(getConfig(), request, response);
return response;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public Response invoke(HttpRequest request) {
}
}
byte[] responseBody = ahr.content().isEmpty() ? Constants.ZERO_BYTES : ahr.content().array();
Response response = new Response(ahr.status().code(), responseHeaders, responseBody);
Response response = new Response(ahr.status().code(), ahr.status().codeAsText(), responseHeaders, responseBody);
httpLogger.logResponse(config, request, response);
return response;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public class AwsLambdaHandler {
private static final String BODY = "body";
private static final String IS_BASE64_ENCODED = "isBase64Encoded";
private static final String STATUS_CODE = "statusCode";
private static final String STATUS_CODE_TEXT = "statusCodeText";

private final ServerHandler handler;

Expand Down Expand Up @@ -102,6 +103,7 @@ public void handle(InputStream in, OutputStream out) throws IOException {
Response response = handler.handle(request);
Map<String, Object> res = new HashMap(4);
res.put(STATUS_CODE, response.getStatus());
res.put(STATUS_CODE_TEXT, response.getStatusText());
Map<String, List<String>> responseHeaders = response.getHeaders();
if (responseHeaders != null) {
Map<String, String> temp = new HashMap(responseHeaders.size());
Expand Down
14 changes: 14 additions & 0 deletions karate-core/src/main/java/com/intuit/karate/http/HttpLogger.java
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,20 @@ public static String getStatusFailureMessage(int expected, Config config, HttpRe
+ ", response: \n" + rawResponse;
}

public static String getStatusTextFailureMessage(String expected, Config config, HttpRequest request, Response response) {
String url = request.getUrl();
HttpLogModifier logModifier = logModifier(config, url);
String maskedUrl = logModifier == null ? url : logModifier.uri(url);
String rawResponse = response.getBodyAsString();
if (rawResponse != null && logModifier != null) {
rawResponse = logModifier.response(url, rawResponse);
}
long responseTime = request.getEndTime() - request.getStartTime();
return "status text was: " + response.getStatusText() + ", expected: " + expected
+ ", response time in milliseconds: " + responseTime + ", url: " + maskedUrl
+ ", response: \n" + rawResponse;
}

public void logRequest(Config config, HttpRequest request) {
requestCount++;
String uri = request.getUrl();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ protected Response handle() {
if (valid == null || !valid) {
logger.error("unauthorized request: {}", request);
response.setStatus(401); // just for logging in finally block
return response().buildWithStatus(401);
return response().buildWithStatus(401, "Unauthorized");
}
}
if (context.isApi()) {
Expand All @@ -194,7 +194,7 @@ protected Response handle() {
} catch (Exception e) {
logger.error("handle failed: {}", e.getMessage());
response.setStatus(500); // just for logging in finally block
return response().buildWithStatus(500);
return response().buildWithStatus(500, "Internal Server Error");
} finally {
close();
if (logger.isDebugEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public Response handle(Request request) {
} else {
rb.locationHeader(signInPath());
}
return rb.buildWithStatus(302);
return rb.buildWithStatus(302, "Found");
}
}
}
Expand Down
35 changes: 30 additions & 5 deletions karate-core/src/main/java/com/intuit/karate/http/Response.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,24 @@ public class Response implements ProxyObject {

private static final Logger logger = LoggerFactory.getLogger(Response.class);

public static final Response OK = new Response(200);
public static final Response OK = new Response(200, "OK");

private static final String BODY = "body";
private static final String BODY_BYTES = "bodyBytes";
private static final String STATUS = "status";
private static final String STATUS_TEXT = "statusText";
private static final String HEADER = "header";
private static final String HEADERS = "headers";
private static final String HEADER_VALUES = "headerValues";
private static final String DATA_TYPE = "dataType";
private static final String RESPONSE_TIME = "responseTime";

private static final String[] KEYS = new String[]{STATUS, HEADER, HEADERS, HEADER_VALUES, BODY, DATA_TYPE, BODY_BYTES, RESPONSE_TIME};
private static final String[] KEYS = new String[]{STATUS, STATUS_TEXT, HEADER, HEADERS, HEADER_VALUES, BODY, DATA_TYPE, BODY_BYTES, RESPONSE_TIME};
private static final Set<String> KEY_SET = new HashSet(Arrays.asList(KEYS));
private static final JsArray KEY_ARRAY = new JsArray(KEYS);

private int status;
private String statusText;
private Map<String, List<String>> headers;
private Object body;

Expand All @@ -76,15 +78,21 @@ public class Response implements ProxyObject {
private long responseTime;

public Response(int status) {
this(status, null);
}

public Response(int status, String statusText) {
this.status = status;
this.statusText = statusText;
}

public Response(int status, Map<String, List<String>> headers, byte[] body) {
this(status, headers, body, null);
public Response(int status, String statusText, Map<String, List<String>> headers, byte[] body) {
this(status, statusText, headers, body, null);
}

public Response(int status, Map<String, List<String>> headers, byte[] body, ResourceType resourceType) {
public Response(int status,String statusText, Map<String, List<String>> headers, byte[] body, ResourceType resourceType) {
this.status = status;
this.statusText = statusText;
this.headers = headers;
this.body = body;
this.resourceType = resourceType;
Expand All @@ -98,6 +106,14 @@ public void setStatus(int status) {
this.status = status;
}

public String getStatusText() {
return statusText;
}

public void setStatusText(String statusText) {
this.statusText = statusText;
}

public int getDelay() {
return delay;
}
Expand Down Expand Up @@ -253,6 +269,8 @@ public Object getMember(String key) {
switch (key) {
case STATUS:
return status;
case STATUS_TEXT:
return statusText;
case HEADER:
return HEADER_FUNCTION;
case HEADERS:
Expand Down Expand Up @@ -284,6 +302,7 @@ public Object getMember(String key) {
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap();
map.put(STATUS, status);
map.put(STATUS_TEXT, statusText);
map.put(HEADERS, JsonUtils.simplify(headers));
map.put(BODY, getBodyConverted());
map.put(RESPONSE_TIME, responseTime);
Expand All @@ -309,6 +328,9 @@ public void putMember(String key, Value value) {
case STATUS:
status = value.asInt();
break;
case STATUS_TEXT:
statusText = value.asString();
break;
case HEADERS:
setHeaders((Map) JsValue.toJava(value));
break;
Expand All @@ -321,6 +343,9 @@ public void putMember(String key, Value value) {
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[status: ").append(status);
if (statusText != null) {
sb.append(", text: ").append(statusText);
}
sb.append(", responseTime: ").append(responseTime);
if (resourceType != null && resourceType != ResourceType.BINARY) {
sb.append(", type: ").append(resourceType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ public Response build() {
}
}
}
return buildWithStatus(response.getStatus());
return buildWithStatus(response.getStatus(), response.getStatusText());
}

private static byte[] merge(byte[] body, byte[] extra) {
Expand Down Expand Up @@ -211,11 +211,11 @@ public Response buildStatic(Request request) { // TODO ETag header handling
} catch (Exception e) {
logger.error("local resource failed: {} - {}", request, e.toString());
}
return buildWithStatus(200);
return buildWithStatus(200, "OK");
}

public Response buildWithStatus(int status) {
return new Response(status, headers, status == 204 ? null : body, resourceType);
public Response buildWithStatus(int status, String statusText) {
return new Response(status, statusText, headers, status == 204 ? null : body, resourceType);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,19 @@ void testResponseStatus() {
match(response.getBodyConverted(), "{ success: false }");
match(response.getStatus(), 404);
}

@Test
void testResponseStatusText() {
background().scenario(
"pathMatches('/hello')",
"def response = { success: false }",
"def responseStatusText = 'Not Found'"
);
request.path("/hello");
handle();
match(response.getBodyConverted(), "{ success: false }");
match(response.getStatusText(), "Not Found");
}

@Test
void testResponseHeaders() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ Given url mockServerUrl + 'cats'
And request { name: 'Billie' }
When method postMethod
Then status 201
And status text Created
And match response == { id: '#ignore', name: 'Billie' }
# And assert responseTime < 1000

Given path response.id
When method getMethod
Then status 200
And status text OK
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ And path 'patch'
And request { foo: 'bar' }
When method patch
Then status 422
And status text Unprocessable Entity
And match response == { success: true }
1 change: 1 addition & 0 deletions karate-demo/src/test/java/demo/cats/cats.feature
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Given path 'cats'
And request { name: 'Billie' }
When method post
Then status 200
And status text
And match response == { id: '#number', name: 'Billie' }

* def id = response.id
Expand Down