Skip to content

Commit 04ea3d7

Browse files
authored
Bug fixes (#16)
- Fix bug where HTTP error data is not being stored as expected in cassette, causing empty error stream on replay - Fix bug with expiration time frames not handling non-never/forever enums correctly (throwing NullPointerException)
1 parent f50b92a commit 04ea3d7

File tree

8 files changed

+83
-46
lines changed

8 files changed

+83
-46
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# CHANGELOG
22

3+
## v0.4.2 (2022-10-20)
4+
5+
- Fix a bug where the error data of a bad HTTP request (4xx or 5xx) was not stored as expected in cassettes, causing
6+
empty error streams on replay.
7+
- Error data for a bad HTTP request is now stored as the "body" in the cassette just like a good HTTP request
8+
would, rather than needlessly stored in a separate "error" key. This more closely matches the behavior of EasyVCR C#.
9+
- This is a breaking change for previously-recorded "error" cassettes, which will no longer replay as expected and
10+
will need to be re-recorded (although likely never worked as expected in the first place).
11+
- Fix a bug where using any expiration time frame other than "forever" and "never" would throw a NullPointerException.
12+
313
## v0.4.1 (2022-10-19)
414

515
- Fix a bug where the error stream of a bad HTTP request (4xx or 5xx) was not properly recreated on replay.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.4.1
1+
0.4.2

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<groupId>com.easypost</groupId>
88
<artifactId>easyvcr</artifactId>
99

10-
<version>0.4.1</version>
10+
<version>0.4.2</version>
1111
<packaging>jar</packaging>
1212

1313
<name>com.easypost:easyvcr</name>

src/main/java/com/easypost/easyvcr/TimeFrame.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,19 @@ public boolean hasLapsed(long fromTimeEpochTimestamp) {
8989
* @return Starting time plus this time frame.
9090
*/
9191
private Instant timePlusFrame(Instant fromTime) {
92+
// We need to do a null check here. The "default" case in the switch statement below doesn't handle null enums.
93+
if (commonTimeFrame == null) { // No common time frame was used
94+
return fromTime.plus(days, ChronoUnit.DAYS).plus(hours, ChronoUnit.HOURS).plus(minutes, ChronoUnit.MINUTES)
95+
.plus(seconds, ChronoUnit.SECONDS);
96+
}
9297
switch (commonTimeFrame) {
9398
case Forever:
9499
return Instant.MAX; // will always been in the future
95100
case Never:
96101
return Instant.MIN; // will always been in the past
97102
default:
103+
// We should never get here, since there should always either be an accounted-for enum,
104+
// or a null value (handled above).
98105
return fromTime.plus(days, ChronoUnit.DAYS).plus(hours, ChronoUnit.HOURS)
99106
.plus(minutes, ChronoUnit.MINUTES).plus(seconds, ChronoUnit.SECONDS);
100107
}

src/main/java/com/easypost/easyvcr/interactionconverters/HttpUrlConnectionInteractionConverter.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,10 @@ public ResponseAndTime createRecordedResponse(HttpURLConnection connection, Cens
8080
String uriString = connection.getURL().toString();
8181
Map<String, List<String>> headers = connection.getHeaderFields();
8282
String body = null;
83-
String errors = null;
8483
try {
8584
body = readFromInputStream(connection.getInputStream());
8685
} catch (NullPointerException | IOException ignored) { // nothing in body if bad status code from server
87-
errors = readFromInputStream(connection.getErrorStream());
86+
body = readFromInputStream(connection.getErrorStream());
8887
}
8988

9089
// apply censors
@@ -101,10 +100,6 @@ public ResponseAndTime createRecordedResponse(HttpURLConnection connection, Cens
101100
body = censors.applyBodyParameterCensors(body);
102101
response.setBody(body);
103102
}
104-
if (errors != null) {
105-
errors = censors.applyBodyParameterCensors(errors);
106-
response.setErrors(errors);
107-
}
108103

109104
return new ResponseAndTime(response, milliseconds);
110105
} catch (URISyntaxException | IOException ignored) {

src/main/java/com/easypost/easyvcr/internalutilities/ExpirationActionExtensions.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public static void checkCompatibleSettings(ExpirationActions action, Mode mode)
1616
throws RecordingExpirationException {
1717
if (action == ExpirationActions.RecordAgain && mode == Mode.Replay) {
1818
throw new RecordingExpirationException(
19-
"Cannot use the Record_Again expiration action in combination with Replay mode.");
19+
"Cannot use the RecordAgain expiration action in combination with Replay mode.");
2020
}
2121
}
2222
}

src/main/java/com/easypost/easyvcr/requestelements/Response.java

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,6 @@ public final class Response extends HttpElement {
4747
*/
4848
private Status status;
4949

50-
/**
51-
* The errors of the response.
52-
*/
53-
private String errors;
54-
5550
/**
5651
* The URI of the response.
5752
*/
@@ -399,24 +394,6 @@ public void setStatus(Status status) {
399394
this.status = status;
400395
}
401396

402-
/**
403-
* Returns the errors of the response.
404-
*
405-
* @return the errors of the response
406-
*/
407-
public String getErrors() {
408-
return this.errors;
409-
}
410-
411-
/**
412-
* Sets the errors of the response.
413-
*
414-
* @param errors the errors of the response
415-
*/
416-
public void setErrors(String errors) {
417-
this.errors = errors;
418-
}
419-
420397
/**
421398
* Returns the URI of the response.
422399
*

src/test/java/HttpUrlConnectionTest.java

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@
2727

2828
import static com.easypost.easyvcr.internalutilities.Tools.readFromInputStream;
2929

30-
public class HttpUrlConnectionTest {
30+
public class HttpUrlConnectionTest {
3131

32-
private static FakeDataService.IPAddressData getIPAddressDataRequest(Cassette cassette, Mode mode) throws Exception {
32+
private static FakeDataService.IPAddressData getIPAddressDataRequest(Cassette cassette, Mode mode)
33+
throws Exception {
3334
RecordableHttpsURLConnection connection = TestUtils.getSimpleHttpsURLConnection(cassette.name, mode, null);
3435

3536
FakeDataService.HttpsUrlConnection fakeDataService = new FakeDataService.HttpsUrlConnection(connection);
@@ -42,7 +43,8 @@ public void testPOSTRequest() throws Exception {
4243
AdvancedSettings advancedSettings = new AdvancedSettings();
4344
advancedSettings.matchRules = new MatchRules().byMethod().byBody().byFullUrl();
4445
RecordableHttpsURLConnection connection =
45-
TestUtils.getSimpleHttpsURLConnection("https://www.google.com", "test_post_request", Mode.Record, advancedSettings);
46+
TestUtils.getSimpleHttpsURLConnection("https://www.google.com", "test_post_request", Mode.Record,
47+
advancedSettings);
4648
String jsonInputString = "{'name': 'Upendra', 'job': 'Programmer'}";
4749
connection.setDoOutput(true);
4850
connection.setRequestMethod("POST");
@@ -70,7 +72,8 @@ public void testNonJsonDataWithCensors() throws Exception {
7072

7173
advancedSettings.matchRules = new MatchRules().byMethod().byBody().byFullUrl();
7274
RecordableHttpsURLConnection connection =
73-
TestUtils.getSimpleHttpsURLConnection("https://www.google.com", "test_non_json", Mode.Record, advancedSettings);
75+
TestUtils.getSimpleHttpsURLConnection("https://www.google.com", "test_non_json", Mode.Record,
76+
advancedSettings);
7477
String jsonInputString = "{'name': 'Upendra', 'job': 'Programmer'}";
7578
connection.setDoOutput(true);
7679
connection.setRequestMethod("POST");
@@ -164,7 +167,8 @@ public void testInteractionElements() throws Exception {
164167

165168
// Most elements of a VCR request are black-boxed, so we can't test them here.
166169
// Instead, we can get the recreated HttpResponseMessage and check the details.
167-
RecordableHttpsURLConnection response = (RecordableHttpsURLConnection) fakeDataService.getIPAddressDataRawResponse();
170+
RecordableHttpsURLConnection response =
171+
(RecordableHttpsURLConnection) fakeDataService.getIPAddressDataRawResponse();
168172
Assert.assertNotNull(response);
169173
}
170174

@@ -193,7 +197,8 @@ public void testCensors() throws Exception {
193197
connection = (RecordableHttpsURLConnection) HttpClients.newClient(HttpClientType.HttpsUrlConnection,
194198
FakeDataService.URL, cassette, Mode.Replay, advancedSettings);
195199
fakeDataService = new FakeDataService.HttpsUrlConnection(connection);
196-
RecordableHttpsURLConnection response = (RecordableHttpsURLConnection) fakeDataService.getIPAddressDataRawResponse();
200+
RecordableHttpsURLConnection response =
201+
(RecordableHttpsURLConnection) fakeDataService.getIPAddressDataRawResponse();
197202

198203
// check that the replayed response contains the censored header
199204
Assert.assertNotNull(response);
@@ -221,7 +226,8 @@ public void testMatchSettings() throws Exception {
221226
connection.setRequestProperty("X-Custom-Header",
222227
"custom-value"); // add custom header to request, shouldn't matter when matching by default rules
223228
fakeDataService = new FakeDataService.HttpsUrlConnection(connection);
224-
RecordableHttpsURLConnection response = (RecordableHttpsURLConnection) fakeDataService.getIPAddressDataRawResponse();
229+
RecordableHttpsURLConnection response =
230+
(RecordableHttpsURLConnection) fakeDataService.getIPAddressDataRawResponse();
225231
Assert.assertNotNull(response);
226232

227233
// replay cassette with custom match rules, should not find a match because request is different (throw exception)
@@ -290,7 +296,8 @@ public void testIgnoreElementsFailMatch() throws URISyntaxException, IOException
290296
String bodyData2 = "{'name': 'NewName', 'job': 'Programmer'}";
291297

292298
// record baseline request first
293-
RecordableHttpsURLConnection connection = HttpClients.newHttpsURLConnection(FakeDataService.URL, cassette, Mode.Record);
299+
RecordableHttpsURLConnection connection =
300+
HttpClients.newHttpsURLConnection(FakeDataService.URL, cassette, Mode.Record);
294301
connection.setDoOutput(true);
295302
connection.setRequestMethod("POST");
296303
// use bodyData1 to make request
@@ -339,7 +346,8 @@ public void testIgnoreElementsPassMatch() throws URISyntaxException, IOException
339346
String bodyData2 = "{'name': 'NewName', 'job': 'Programmer'}";
340347

341348
// record baseline request first
342-
RecordableHttpsURLConnection connection = HttpClients.newHttpsURLConnection(FakeDataService.URL, cassette, Mode.Record);
349+
RecordableHttpsURLConnection connection =
350+
HttpClients.newHttpsURLConnection(FakeDataService.URL, cassette, Mode.Record);
343351
connection.setDoOutput(true);
344352
connection.setRequestMethod("POST");
345353
// use bodyData1 to make request
@@ -383,7 +391,7 @@ public void testIgnoreElementsPassMatch() throws URISyntaxException, IOException
383391
}
384392

385393
@Test
386-
public void testExpirationSettings() throws Exception {
394+
public void testExpirationSettingsCommonTimeFrame() throws Exception {
387395
Cassette cassette = TestUtils.getCassette("test_expiration_settings");
388396
cassette.erase(); // Erase cassette before recording
389397

@@ -398,7 +406,8 @@ public void testExpirationSettings() throws Exception {
398406
connection = (RecordableHttpsURLConnection) HttpClients.newClient(HttpClientType.HttpsUrlConnection,
399407
FakeDataService.URL, cassette, Mode.Replay);
400408
fakeDataService = new FakeDataService.HttpsUrlConnection(connection);
401-
RecordableHttpsURLConnection response = (RecordableHttpsURLConnection) fakeDataService.getIPAddressDataRawResponse();
409+
RecordableHttpsURLConnection response =
410+
(RecordableHttpsURLConnection) fakeDataService.getIPAddressDataRawResponse();
402411
Assert.assertNotNull(response);
403412

404413
// replay cassette with custom expiration rules, should not find a match because recording is expired (throw exception)
@@ -416,10 +425,49 @@ public void testExpirationSettings() throws Exception {
416425
// replay cassette with bad expiration rules, should throw an exception because settings are bad
417426
advancedSettings = new AdvancedSettings();
418427
advancedSettings.timeFrame = TimeFrame.never();
419-
advancedSettings.whenExpired = ExpirationActions.RecordAgain; // invalid settings for replay mode, should throw exception
428+
advancedSettings.whenExpired =
429+
ExpirationActions.RecordAgain; // invalid settings for replay mode, should throw exception
420430
AdvancedSettings finalAdvancedSettings = advancedSettings;
421-
Assert.assertThrows(RecordingExpirationException.class, () -> HttpClients.newClient(HttpClientType.HttpsUrlConnection,
422-
FakeDataService.URL, cassette, Mode.Replay, finalAdvancedSettings));
431+
Assert.assertThrows(RecordingExpirationException.class,
432+
() -> HttpClients.newClient(HttpClientType.HttpsUrlConnection, FakeDataService.URL, cassette,
433+
Mode.Replay, finalAdvancedSettings));
434+
}
435+
436+
@Test
437+
public void testExpirationSettingsCustomTimeFrame() throws Exception {
438+
Cassette cassette = TestUtils.getCassette("test_expiration_settings");
439+
cassette.erase(); // Erase cassette before recording
440+
441+
// record cassette first
442+
RecordableHttpsURLConnection connection =
443+
(RecordableHttpsURLConnection) HttpClients.newClient(HttpClientType.HttpsUrlConnection,
444+
FakeDataService.URL, cassette, Mode.Record);
445+
FakeDataService.HttpsUrlConnection fakeDataService = new FakeDataService.HttpsUrlConnection(connection);
446+
fakeDataService.getIPAddressDataRawResponse();
447+
448+
// Custom expiration rules
449+
AdvancedSettings advancedSettings = new AdvancedSettings();
450+
advancedSettings.timeFrame = new TimeFrame(1, 0, 0, 0); // 1 day from now
451+
advancedSettings.whenExpired = ExpirationActions.ThrowException; // throw exception when recording is expired
452+
453+
// replay cassette with custom expiration rules, should find a match
454+
connection = (RecordableHttpsURLConnection) HttpClients.newClient(HttpClientType.HttpsUrlConnection,
455+
FakeDataService.URL, cassette, Mode.Replay, advancedSettings);
456+
fakeDataService = new FakeDataService.HttpsUrlConnection(connection);
457+
RecordableHttpsURLConnection response =
458+
(RecordableHttpsURLConnection) fakeDataService.getIPAddressDataRawResponse();
459+
Assert.assertNotNull(response);
460+
461+
// Change expiration rules
462+
advancedSettings.timeFrame = new TimeFrame(-1, 0, 0, 0); // 1 day ago
463+
464+
// replay cassette with custom expiration rules, should not find a match because recording is expired (throw exception)
465+
connection = (RecordableHttpsURLConnection) HttpClients.newClient(HttpClientType.HttpsUrlConnection,
466+
FakeDataService.URL, cassette, Mode.Replay, advancedSettings);
467+
fakeDataService = new FakeDataService.HttpsUrlConnection(connection);
468+
FakeDataService.HttpsUrlConnection finalFakeDataService = fakeDataService;
469+
// this throws a RuntimeException rather than a RecordingExpirationException because the exceptions are coalesced internally
470+
Assert.assertThrows(Exception.class, () -> finalFakeDataService.getIPAddressData());
423471
}
424472

425473
@Test

0 commit comments

Comments
 (0)