diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8dc1688..22285aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,20 @@
# CHANGELOG
+## Next Release
+
+- Add support for censoring XML, HTML and plain text bodies
+- [BREAKING CHANGE] `CensorElement` now abstract base class, cannot be used directly
+ - Three types of `CensorElement` options available:
+ - `KeyCensorElement`: Censor the value of a specified key (will be ignored if used for plain text/HTML data)
+ - `RegexCensorElement`: Censor any string that matches a specified regex pattern (will check the value of a key-value pair if used for JSON/XML data)
+ - `TextCensorElement`: Censor a specified string (will check the value of a key-value pair if used for JSON/XML data; requires the whole body to match the specified string if used for plain text/HTML data)
+ - Body censoring: `KeyCensorElement` (recommended for JSON/XML if key is known), `TextCensorElement` (recommended for JSON/XML if value is known), and `RegexCensorElement` (recommended for plain text/HTML)
+ - Path element censoring: Use `RegexCensorElement`
+ - Query parameter censoring: Use `KeyCensorElement`
+ - Header censoring: Use `KeyCensorElement`
+- [BREAKING CHANGE] `CensorHeadersByKeys`, `CensorBodyElementsByKeys`, `CensorQueryParametersByKeys` and `CensorPathElementsByPatterns` removed
+ - Use `CensorHeaders`, `CensorBodyElements`, `CensorQueryParameters` and `CensorPathElements` instead
+
## v0.9.0 (2023-05-17)
- Fix a bug where URLs were not being extracted correctly, potentially causing false matches when matching by URL
diff --git a/EasyVCR.Tests/CensorsTest.cs b/EasyVCR.Tests/CensorsTest.cs
index 045e336..4341c47 100644
--- a/EasyVCR.Tests/CensorsTest.cs
+++ b/EasyVCR.Tests/CensorsTest.cs
@@ -1,4 +1,8 @@
+using System;
using System.Collections.Generic;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Xml;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace EasyVCR.Tests
@@ -201,5 +205,337 @@ public void TestApplyPathElementsCensorsNoCensorsReturnsOriginalUrl()
Assert.AreEqual(url, result);
}
+
+ ///
+ /// Test TextCensorElement works for XML bodies
+ ///
+ [TestMethod]
+ public async Task TestTextCensorOnXml()
+ {
+ var cassette = TestUtils.GetCassette("test_text_censor_on_xml");
+ cassette.Erase(); // Erase cassette before recording
+
+ // set up advanced settings
+ var censorString = new Guid().ToString(); // generate random string, high chance of not being in original data
+ var advancedSettings = new AdvancedSettings
+ {
+ Censors = new Censors(censorString).CensorBodyElements(
+ new List
+ {
+ // censor the word "r/ProgrammerHumor"
+ new TextCensorElement("r/ProgrammerHumor", false),
+ }),
+ };
+
+ // record cassette with advanced settings first
+ var client = HttpClients.NewHttpClient(cassette, Mode.Record, advancedSettings);
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetXmlDataRawResponse();
+
+ // now replay cassette
+ client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
+ fakeDataService = new FakeDataService(client);
+ var xmlData = await fakeDataService.GetXmlData();
+
+ Assert.IsNotNull(xmlData);
+ var xmlDocument = new XmlDocument();
+ xmlDocument.LoadXml(xmlData);
+
+ // word "r/ProgrammerHumor" should be censored
+ // for testing purposes, we know this is the "label" property of the "category" node under "feed"
+ var categoryNode = xmlDocument.FirstChild?.FirstChild;
+ Assert.IsNotNull(categoryNode);
+ Assert.AreEqual(censorString, categoryNode.Attributes["label"].Value);
+ }
+
+ ///
+ /// Test KeyCensorElement works for XML bodies
+ ///
+ [TestMethod]
+ public async Task TestKeyCensorOnXml()
+ {
+ var cassette = TestUtils.GetCassette("test_key_censor_on_xml");
+ cassette.Erase(); // Erase cassette before recording
+
+ // set up advanced settings
+ var censorString = new Guid().ToString(); // generate random string, high chance of not being in original data
+ var advancedSettings = new AdvancedSettings
+ {
+ Censors = new Censors(censorString).CensorBodyElements(
+ new List
+ {
+ // censor the value of the "title" key
+ new KeyCensorElement("title", false),
+ }),
+ };
+
+ // record cassette with advanced settings first
+ var client = HttpClients.NewHttpClient(cassette, Mode.Record, advancedSettings);
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetXmlDataRawResponse();
+
+ // now replay cassette
+ client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
+ fakeDataService = new FakeDataService(client);
+ var xmlData = await fakeDataService.GetXmlData();
+
+ Assert.IsNotNull(xmlData);
+ var xmlDocument = new XmlDocument();
+ xmlDocument.LoadXml(xmlData);
+
+ // whole value of "title" key should be censored
+ var nodes = xmlDocument.SelectNodes("//title");
+ Assert.IsNotNull(nodes);
+ foreach (XmlNode node in nodes)
+ {
+ Assert.AreEqual(censorString, node.InnerText);
+ }
+ }
+
+ ///
+ /// Test RegexCensorElement works for XML bodies
+ ///
+ [TestMethod]
+ public async Task TestRegexCensorOnXml()
+ {
+ var cassette = TestUtils.GetCassette("test_regex_censor_on_xml");
+ cassette.Erase(); // Erase cassette before recording
+
+ // set up advanced settings
+ var censorString = new Guid().ToString(); // generate random string, high chance of not being in original data
+ var advancedSettings = new AdvancedSettings
+ {
+ Censors = new Censors(censorString).CensorBodyElements(
+ new List
+ {
+ // censor any value that looks like an date stamp
+ new RegexCensorElement(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", false),
+ }),
+ };
+
+ // record cassette with advanced settings first
+ var client = HttpClients.NewHttpClient(cassette, Mode.Record, advancedSettings);
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetXmlDataRawResponse();
+
+ // now replay cassette
+ client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
+ fakeDataService = new FakeDataService(client);
+ var xmlData = await fakeDataService.GetXmlData();
+
+ Assert.IsNotNull(xmlData);
+ var xmlDocument = new XmlDocument();
+ xmlDocument.LoadXml(xmlData);
+
+ // all values that look like urls should be censored
+ // for testing purposes, we know this is stored in the "uri" nodes
+ var nodes = xmlDocument.SelectNodes("//uri");
+ Assert.IsNotNull(nodes);
+ foreach (XmlNode node in nodes)
+ {
+ Assert.AreEqual(censorString, node.InnerText);
+ }
+ }
+
+ [Ignore("Hard to test")]
+ [TestMethod]
+ public async Task TestTextCensorOnHtml()
+ {
+ // TextCensorHTML censors the whole text in the HTML body
+ // Would need an HTML page with a small body to test this
+ Assert.Fail();
+ }
+
+ [Ignore("Can't use KeyCensorElement on HTML bodies")]
+ [TestMethod]
+ public async Task TestKeyCensorOnHtml()
+ {
+ Assert.Fail("Can't use KeyCensorElement on HTML bodies");
+ }
+
+ [TestMethod]
+ public async Task TestRegexCensorOnHtml()
+ {
+ var cassette = TestUtils.GetCassette("test_regex_censor_on_html");
+ cassette.Erase(); // Erase cassette before recording
+
+ // set up advanced settings
+ var censorString = new Guid().ToString(); // generate random string, high chance of not being in original data
+ const string pattern = ".*";
+ var advancedSettings = new AdvancedSettings
+ {
+ Censors = new Censors(censorString).CensorBodyElements(
+ new List
+ {
+ // censor the pattern
+ new RegexCensorElement(pattern, false),
+ }),
+ };
+
+ // record cassette with advanced settings first
+ var client = HttpClients.NewHttpClient(cassette, Mode.Record, advancedSettings);
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetHtmlDataRawResponse();
+
+ // now replay cassette
+ client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
+ fakeDataService = new FakeDataService(client);
+ var textData = await fakeDataService.GetHtmlData();
+
+ Assert.IsNotNull(textData);
+
+ // censored pattern should no longer exist, and censor string should exist
+ Assert.IsFalse(Regex.IsMatch(textData, pattern));
+ Assert.IsTrue(textData.Contains(censorString));
+ }
+
+ ///
+ /// Test TextCensorElement works for plain text bodies
+ ///
+ [TestMethod]
+ public async Task TestTextCensorOnText()
+ {
+ var cassette = TestUtils.GetCassette("test_text_censor_on_text");
+ cassette.Erase(); // Erase cassette before recording
+
+ // set up advanced settings
+ var censorString = new Guid().ToString(); // generate random string, high chance of not being in original data
+ const string textToCensor = "# UGAArchive\nArchives of projects I did as a student at The University of Georgia\n";
+ var advancedSettings = new AdvancedSettings
+ {
+ Censors = new Censors(censorString).CensorBodyElements(
+ new List
+ {
+ // censor the text
+ new TextCensorElement(textToCensor, false),
+ }),
+ };
+
+ // record cassette with advanced settings first
+ var client = HttpClients.NewHttpClient(cassette, Mode.Record, advancedSettings);
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetRawDataRawResponse();
+
+ // now replay cassette
+ client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
+ fakeDataService = new FakeDataService(client);
+ var textData = await fakeDataService.GetRawData();
+
+ Assert.IsNotNull(textData);
+
+ // censored word should no longer exist, and censor string should exist
+ Assert.IsFalse(textData.Contains(textToCensor));
+ Assert.IsTrue(textData.Contains(censorString));
+ }
+
+ [Ignore("Can't use KeyCensorElement on plain text bodies")]
+ [TestMethod]
+ public async Task TestKeyCensorOnText()
+ {
+ Assert.Fail("Can't use KeyCensorElement on plain text bodies");
+ }
+
+ ///
+ /// Test RegexCensorElement works for plain text bodies
+ ///
+ [TestMethod]
+ public async Task TestRegexCensorOnText()
+ {
+ var cassette = TestUtils.GetCassette("test_regex_censor_on_text");
+ cassette.Erase(); // Erase cassette before recording
+
+ // set up advanced settings
+ var censorString = new Guid().ToString(); // generate random string, high chance of not being in original data
+ const string pattern = "^# UGAArchive";
+ var advancedSettings = new AdvancedSettings
+ {
+ Censors = new Censors(censorString).CensorBodyElements(
+ new List
+ {
+ // censor the pattern
+ new RegexCensorElement(pattern, false),
+ }),
+ };
+
+ // record cassette with advanced settings first
+ var client = HttpClients.NewHttpClient(cassette, Mode.Record, advancedSettings);
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetRawDataRawResponse();
+
+ // now replay cassette
+ client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
+ fakeDataService = new FakeDataService(client);
+ var textData = await fakeDataService.GetRawData();
+
+ Assert.IsNotNull(textData);
+
+ // censored pattern should no longer exist, and censor string should exist
+ Assert.IsFalse(Regex.IsMatch(textData, pattern));
+ Assert.IsTrue(textData.Contains(censorString));
+ }
+
+ ///
+ /// Test that we can mix and match censor elements
+ ///
+ [TestMethod]
+ public async Task TestMixAndMatchCensorElements()
+ {
+ var cassette = TestUtils.GetCassette("test_mix_and_match_censor_elements");
+ cassette.Erase(); // Erase cassette before recording
+
+ // set up advanced settings
+ var censorString = new Guid().ToString(); // generate random string, high chance of not being in original data
+ var advancedSettings = new AdvancedSettings
+ {
+ Censors = new Censors(censorString).CensorBodyElements(
+ new List
+ {
+ // censor the word "r/ProgrammerHumor"
+ new TextCensorElement("r/ProgrammerHumor", false),
+ // censor the value of the "title" key
+ new KeyCensorElement("title", false),
+ // censor any value that looks like an date stamp
+ new RegexCensorElement(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", false),
+ }),
+ };
+
+ // record cassette with advanced settings first
+ var client = HttpClients.NewHttpClient(cassette, Mode.Record, advancedSettings);
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetXmlDataRawResponse();
+
+ // now replay cassette
+ client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
+ fakeDataService = new FakeDataService(client);
+ var xmlData = await fakeDataService.GetXmlData();
+
+ // check that the xml data was censored
+ Assert.IsNotNull(xmlData);
+ var xmlDocument = new XmlDocument();
+ xmlDocument.LoadXml(xmlData);
+
+ // word "r/ProgrammerHumor" should be censored
+ // for testing purposes, we know this is the "label" property of the "category" node under "feed"
+ var categoryNode = xmlDocument.FirstChild?.FirstChild;
+ Assert.IsNotNull(categoryNode);
+ Assert.AreEqual(censorString, categoryNode.Attributes["label"].Value);
+
+ // whole value of "title" key should be censored
+ var nodes = xmlDocument.SelectNodes("//title");
+ Assert.IsNotNull(nodes);
+ foreach (XmlNode node in nodes)
+ {
+ Assert.AreEqual(censorString, node.InnerText);
+ }
+
+ // all values that look like urls should be censored
+ // for testing purposes, we know this is stored in the "uri" nodes
+ nodes = xmlDocument.SelectNodes("//uri");
+ Assert.IsNotNull(nodes);
+ foreach (XmlNode node in nodes)
+ {
+ Assert.AreEqual(censorString, node.InnerText);
+ }
+ }
}
}
diff --git a/EasyVCR.Tests/ClientTest.cs b/EasyVCR.Tests/ClientTest.cs
index d6f2f6b..86c2544 100644
--- a/EasyVCR.Tests/ClientTest.cs
+++ b/EasyVCR.Tests/ClientTest.cs
@@ -6,6 +6,7 @@
using System.Text;
using System.Threading.Tasks;
using System.Web;
+using System.Xml;
using EasyVCR.Handlers;
using EasyVCR.RequestElements;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -89,13 +90,12 @@ public async Task TestAutoMode()
cassette.Erase(); // Erase cassette before recording
// in replay mode, if cassette is empty, should throw an exception
- await Assert.ThrowsExceptionAsync(async () => await GetIPAddressDataRequest(cassette, Mode.Replay));
+ await Assert.ThrowsExceptionAsync(async () => await GetJsonDataRequest(cassette, Mode.Replay));
Assert.IsTrue(cassette.NumInteractions == 0); // Make sure cassette is still empty
// in auto mode, if cassette is empty, should make and record a real request
- var summary = await GetIPAddressDataRequest(cassette, Mode.Auto);
- Assert.IsNotNull(summary);
- Assert.IsNotNull(summary.IPAddress);
+ var responseBody = await GetJsonDataRequest(cassette, Mode.Auto);
+ Assert.IsNotNull(responseBody);
Assert.IsTrue(cassette.NumInteractions > 0); // Make sure cassette is no longer empty
}
@@ -107,20 +107,21 @@ public async Task TestCensors()
// set up advanced settings
const string censorString = "censored-by-test";
+ var headerCensors = new List { new KeyCensorElement("Date", false) };
var advancedSettings = new AdvancedSettings
{
- Censors = new Censors(censorString).CensorHeadersByKeys(new List { "Date" })
+ Censors = new Censors(censorString).CensorHeaders(headerCensors),
};
// record cassette with advanced settings first
var client = HttpClients.NewHttpClient(cassette, Mode.Record, advancedSettings);
- var fakeDataService = new FakeJsonDataService(client);
- var _ = await fakeDataService.GetIPAddressDataRawResponse();
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetJsonDataRawResponse();
// now replay cassette
client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
- fakeDataService = new FakeJsonDataService(client);
- var response = await fakeDataService.GetIPAddressDataRawResponse();
+ fakeDataService = new FakeDataService(client);
+ var response = await fakeDataService.GetJsonDataRawResponse();
// check that the replayed response contains the censored header
Assert.IsNotNull(response);
@@ -142,26 +143,27 @@ public async Task TestRegexCensors()
cassette.Erase(); // Erase cassette before recording
// set up regex pattern
- var url = new Uri(FakeDataService.GetIPAddressDataUrl());
+ var url = new Uri(FakeDataService.JsonDataUrl);
var domain = url.Host;
var regexPattern = domain;
// set up advanced settings
const string censorString = "censored-by-test";
+ var pathCensors = new List { new RegexCensorElement(regexPattern, false) };
var advancedSettings = new AdvancedSettings
{
- Censors = new Censors(censorString).CensorPathElementsByPatterns(new List { regexPattern })
+ Censors = new Censors(censorString).CensorPathElements(pathCensors),
};
// record cassette with advanced settings
var client = HttpClients.NewHttpClient(cassette, Mode.Record, advancedSettings);
- var fakeDataService = new FakeJsonDataService(client);
- var _ = await fakeDataService.GetIPAddressDataRawResponse();
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetJsonDataRawResponse();
// verify that censoring does not interfere with replay
client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
- fakeDataService = new FakeJsonDataService(client);
- var response = await fakeDataService.GetIPAddressDataRawResponse();
+ fakeDataService = new FakeDataService(client);
+ var response = await fakeDataService.GetJsonDataRawResponse();
Assert.IsNotNull(response);
}
@@ -249,15 +251,15 @@ public async Task TestDelay()
// record cassette first
var client = HttpClients.NewHttpClient(cassette, Mode.Record);
- var fakeDataService = new FakeJsonDataService(client);
- var _ = await fakeDataService.GetIPAddressDataRawResponse();
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetJsonDataRawResponse();
// baseline - how much time does it take to replay the cassette?
client = HttpClients.NewHttpClient(cassette, Mode.Replay);
- fakeDataService = new FakeJsonDataService(client);
+ fakeDataService = new FakeDataService(client);
var stopwatch = new Stopwatch();
stopwatch.Start();
- var response = await fakeDataService.GetIPAddressDataRawResponse();
+ var response = await fakeDataService.GetJsonDataRawResponse();
stopwatch.Stop();
// confirm the normal replay worked, note time
@@ -271,12 +273,12 @@ public async Task TestDelay()
ManualDelay = delay
};
client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
- fakeDataService = new FakeJsonDataService(client);
+ fakeDataService = new FakeDataService(client);
// time replay request
stopwatch = new Stopwatch();
stopwatch.Start();
- response = await fakeDataService.GetIPAddressDataRawResponse();
+ response = await fakeDataService.GetJsonDataRawResponse();
stopwatch.Stop();
// check that the delay was respected (within margin of error)
@@ -291,7 +293,7 @@ public async Task TestErase()
var cassette = TestUtils.GetCassette("test_erase");
// record something to the cassette
- var _ = await GetIPAddressDataRequest(cassette, Mode.Record);
+ var _ = await GetJsonDataRequest(cassette, Mode.Record);
Assert.IsTrue(cassette.NumInteractions > 0);
// erase the cassette
@@ -306,7 +308,7 @@ public async Task TestEraseAndPlayback()
cassette.Erase(); // Erase cassette before recording
// cassette is empty, so replaying should throw an exception
- await Assert.ThrowsExceptionAsync(async () => await GetIPAddressDataRequest(cassette, Mode.Replay));
+ await Assert.ThrowsExceptionAsync(async () => await GetJsonDataRequest(cassette, Mode.Replay));
}
[TestMethod]
@@ -315,10 +317,9 @@ public async Task TestEraseAndRecord()
var cassette = TestUtils.GetCassette("test_erase_and_record");
cassette.Erase(); // Erase cassette before recording
- var summary = await GetIPAddressDataRequest(cassette, Mode.Record);
+ var responseBody = await GetJsonDataRequest(cassette, Mode.Record);
- Assert.IsNotNull(summary);
- Assert.IsNotNull(summary.IPAddress);
+ Assert.IsNotNull(responseBody);
Assert.IsTrue(cassette.NumInteractions > 0); // Make sure cassette is not empty
}
@@ -330,13 +331,13 @@ public async Task TestExpirationSettings()
// record cassette first
var client = HttpClients.NewHttpClient(cassette, Mode.Record);
- var fakeDataService = new FakeJsonDataService(client);
- await fakeDataService.GetIPAddressDataRawResponse();
+ var fakeDataService = new FakeDataService(client);
+ await fakeDataService.GetJsonDataRawResponse();
// replay cassette with default expiration rules, should find a match
client = HttpClients.NewHttpClient(cassette, Mode.Replay);
- fakeDataService = new FakeJsonDataService(client);
- var response = await fakeDataService.GetIPAddressDataRawResponse();
+ fakeDataService = new FakeDataService(client);
+ var response = await fakeDataService.GetJsonDataRawResponse();
Assert.IsNotNull(response);
// replay cassette with custom expiration rules, should not find a match because recording is expired (throw exception)
@@ -347,8 +348,8 @@ public async Task TestExpirationSettings()
};
Task.Delay(TimeSpan.FromSeconds(1)).Wait(); // Allow 1 second to lapse to ensure recording is now "expired"
client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
- fakeDataService = new FakeJsonDataService(client);
- await Assert.ThrowsExceptionAsync(async () => await fakeDataService.GetIPAddressDataRawResponse());
+ fakeDataService = new FakeDataService(client);
+ await Assert.ThrowsExceptionAsync(async () => await fakeDataService.GetJsonDataRawResponse());
// replay cassette with bad expiration rules, should throw an exception because settings are bad
advancedSettings = new AdvancedSettings
@@ -364,7 +365,7 @@ public void TestFakeDataServiceClient()
{
var client = TestUtils.GetSimpleClient("test_fake_data_service_client", Mode.Bypass);
- var fakeDataService = new FakeJsonDataService(client);
+ var fakeDataService = new FakeDataService(client);
Assert.IsNotNull(fakeDataService.Client);
}
@@ -379,7 +380,7 @@ public async Task TestIgnoreElementsFailMatch()
// record baseline request first
var client = HttpClients.NewHttpClient(cassette, Mode.Record);
- var _ = await client.PostAsync(FakeDataService.GetPreparedIPAddressDataUrl("json"), bodyData1);
+ var _ = await client.PostAsync(FakeDataService.JsonDataUrl, bodyData1);
// try to replay the request with different body data
client = HttpClients.NewHttpClient(cassette, Mode.Replay, new AdvancedSettings
@@ -388,7 +389,7 @@ public async Task TestIgnoreElementsFailMatch()
});
// should fail since we're strictly in replay mode and there's no exact match
- await Assert.ThrowsExceptionAsync(async () => await client.PostAsync(FakeDataService.GetPreparedIPAddressDataUrl("json"), bodyData2));
+ await Assert.ThrowsExceptionAsync(async () => await client.PostAsync(FakeDataService.JsonDataUrl, bodyData2));
}
[TestMethod]
@@ -402,7 +403,7 @@ public async Task TestCustomMatchRule()
// record baseline request first
var client = HttpClients.NewHttpClient(cassette, Mode.Record);
- var _ = await client.PostAsync(FakeDataService.GetPreparedIPAddressDataUrl("json"), bodyData1);
+ var _ = await client.PostAsync(FakeDataService.JsonDataUrl, bodyData1);
// try to replay the request with no custom match rule
client = HttpClients.NewHttpClient(cassette, Mode.Replay, new AdvancedSettings()
@@ -411,7 +412,7 @@ public async Task TestCustomMatchRule()
});
// should pass since it passes the default match rules
- await client.PostAsync(FakeDataService.GetPreparedIPAddressDataUrl("json"), bodyData1);
+ await client.PostAsync(FakeDataService.JsonDataUrl, bodyData1);
// try to replay the request with a custom match rule
client = HttpClients.NewHttpClient(cassette, Mode.Replay, new AdvancedSettings
@@ -420,7 +421,7 @@ public async Task TestCustomMatchRule()
});
// should fail since the custom match rule always returns false and there's never a match
- await Assert.ThrowsExceptionAsync(async () => await client.PostAsync(FakeDataService.GetPreparedIPAddressDataUrl("json"), bodyData2));
+ await Assert.ThrowsExceptionAsync(async () => await client.PostAsync(FakeDataService.JsonDataUrl, bodyData2));
}
[TestMethod]
@@ -434,12 +435,12 @@ public async Task TestIgnoreElementsPassMatch()
// record baseline request first
var client = HttpClients.NewHttpClient(cassette, Mode.Record);
- var _ = await client.PostAsync(FakeDataService.GetPreparedIPAddressDataUrl("json"), bodyData1);
+ var _ = await client.PostAsync(FakeDataService.JsonDataUrl, bodyData1);
// try to replay the request with different body data, but ignoring the differences
var ignoreElements = new List
{
- new CensorElement("name", false)
+ new KeyCensorElement("name", false)
};
client = HttpClients.NewHttpClient(cassette, Mode.Replay, new AdvancedSettings
{
@@ -447,7 +448,7 @@ public async Task TestIgnoreElementsPassMatch()
});
// should succeed since we're ignoring the differences
- var response = await client.PostAsync(FakeDataService.GetPreparedIPAddressDataUrl("json"), bodyData2);
+ var response = await client.PostAsync(FakeDataService.JsonDataUrl, bodyData2);
Assert.IsNotNull(response);
Assert.IsTrue(Utilities.ResponseCameFromRecording(response));
}
@@ -485,11 +486,11 @@ public async Task TestInteractionElements()
cassette.Erase(); // Erase cassette before recording
var client = HttpClients.NewHttpClient(cassette, Mode.Record);
- var fakeDataService = new FakeJsonDataService(client);
+ var fakeDataService = new FakeDataService(client);
// Most elements of a VCR request are black-boxed, so we can't test them here.
// Instead, we can get the recreated HttpResponseMessage and check the details.
- var response = await fakeDataService.GetIPAddressDataRawResponse();
+ var response = await fakeDataService.GetJsonDataRawResponse();
Assert.IsNotNull(response);
}
@@ -501,14 +502,14 @@ public async Task TestMatchSettings()
// record cassette first
var client = HttpClients.NewHttpClient(cassette, Mode.Record);
- var fakeDataService = new FakeJsonDataService(client);
- var _ = await fakeDataService.GetIPAddressDataRawResponse();
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetJsonDataRawResponse();
// replay cassette with default match rules, should find a match
client = HttpClients.NewHttpClient(cassette, Mode.Replay);
client.DefaultRequestHeaders.Add("X-Custom-Header", "custom-value"); // add custom header to request, shouldn't matter when matching by default rules
- fakeDataService = new FakeJsonDataService(client);
- var response = await fakeDataService.GetIPAddressDataRawResponse();
+ fakeDataService = new FakeDataService(client);
+ var response = await fakeDataService.GetJsonDataRawResponse();
Assert.IsNotNull(response);
// replay cassette with custom match rules, should not find a match because request is different (throw exception)
@@ -518,8 +519,8 @@ public async Task TestMatchSettings()
};
client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
client.DefaultRequestHeaders.Add("X-Custom-Header", "custom-value"); // add custom header to request, causing a match failure when matching by everything
- fakeDataService = new FakeJsonDataService(client);
- await Assert.ThrowsExceptionAsync(async () => await fakeDataService.GetIPAddressDataRawResponse());
+ fakeDataService = new FakeDataService(client);
+ await Assert.ThrowsExceptionAsync(async () => await fakeDataService.GetJsonDataRawResponse());
}
[TestMethod]
@@ -531,14 +532,14 @@ public async Task TestHeadersFailMatch()
// record cassette first
var client = HttpClients.NewHttpClient(cassette, Mode.Record);
client.DefaultRequestHeaders.Add("X-Custom-Header", "custom-value1");
- var fakeDataService = new FakeJsonDataService(client);
- var _ = await fakeDataService.GetIPAddressDataRawResponse();
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetJsonDataRawResponse();
// replay cassette with default match rules, should find a match
client = HttpClients.NewHttpClient(cassette, Mode.Replay);
// no custom header to request, shouldn't matter when matching by default rules
- fakeDataService = new FakeJsonDataService(client);
- var response = await fakeDataService.GetIPAddressDataRawResponse();
+ fakeDataService = new FakeDataService(client);
+ var response = await fakeDataService.GetJsonDataRawResponse();
Assert.IsNotNull(response);
// replay cassette with custom match rules, should not find a match because header value is different (throw exception)
@@ -548,8 +549,8 @@ public async Task TestHeadersFailMatch()
};
client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
client.DefaultRequestHeaders.Add("X-Custom-Header", "custom-value2"); // add header with different value to request
- fakeDataService = new FakeJsonDataService(client);
- await Assert.ThrowsExceptionAsync(async () => await fakeDataService.GetIPAddressDataRawResponse());
+ fakeDataService = new FakeDataService(client);
+ await Assert.ThrowsExceptionAsync(async () => await fakeDataService.GetJsonDataRawResponse());
}
[TestMethod]
@@ -561,8 +562,8 @@ public async Task TestMissingHeaderFailMatch()
// record cassette first
var client = HttpClients.NewHttpClient(cassette, Mode.Record);
client.DefaultRequestHeaders.Add("X-Custom-Header", "custom-value");
- var fakeDataService = new FakeJsonDataService(client);
- var _ = await fakeDataService.GetIPAddressDataRawResponse();
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetJsonDataRawResponse();
// replay cassette with custom match rules, should not find a match because header is missing (throw exception)
var advancedSettings = new AdvancedSettings
@@ -570,8 +571,8 @@ public async Task TestMissingHeaderFailMatch()
MatchRules = new MatchRules().ByHeader("X-Custom-Header")
};
client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
- fakeDataService = new FakeJsonDataService(client);
- await Assert.ThrowsExceptionAsync(async () => await fakeDataService.GetIPAddressDataRawResponse());
+ fakeDataService = new FakeDataService(client);
+ await Assert.ThrowsExceptionAsync(async () => await fakeDataService.GetJsonDataRawResponse());
}
[TestMethod]
@@ -584,8 +585,8 @@ public async Task TestHeadersPassMatch()
var client = HttpClients.NewHttpClient(cassette, Mode.Record);
client.DefaultRequestHeaders.Add("X-Custom-Header1", "custom-value1"); // add custom header to request
client.DefaultRequestHeaders.Add("X-Custom-Header2", "custom-value2");
- var fakeDataService = new FakeJsonDataService(client);
- var _ = await fakeDataService.GetIPAddressDataRawResponse();
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetJsonDataRawResponse();
// replay cassette with custom match rules
var advancedSettings = new AdvancedSettings
@@ -594,8 +595,8 @@ public async Task TestHeadersPassMatch()
};
client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
client.DefaultRequestHeaders.Add("X-Custom-Header1", "custom-value1");
- fakeDataService = new FakeJsonDataService(client);
- var response = await fakeDataService.GetIPAddressDataRawResponse();
+ fakeDataService = new FakeDataService(client);
+ var response = await fakeDataService.GetJsonDataRawResponse();
// should succeed since X-Custom-Header1 header value equals
Assert.IsNotNull(response);
@@ -612,7 +613,14 @@ public async Task TestNestedCensoring()
// set up advanced settings
const string censorString = "censored-by-test";
var censors = new Censors(censorString);
- censors.CensorBodyElementsByKeys(new List { "nested_dict_1_1_1", "nested_dict_2_2", "nested_array", "null_key" });
+ var bodyCensors = new List
+ {
+ new KeyCensorElement("nested_dict_1_1_1", false),
+ new KeyCensorElement("nested_dict_2_2", false),
+ new KeyCensorElement("nested_array", false),
+ new KeyCensorElement("null_key", false),
+ };
+ censors.CensorBodyElements(bodyCensors);
var advancedSettings = new AdvancedSettings
{
Censors = censors
@@ -658,44 +666,16 @@ public async Task TestStrictRequestMatching()
Assert.IsTrue(Utilities.ResponseCameFromRecording(response));
}
- [Ignore]
- [TestMethod]
- public async Task TestXmlCensoring()
- {
- var cassette = TestUtils.GetCassette("test_xml_censors");
- cassette.Erase(); // Erase cassette before recording
-
- // set up advanced settings
- const string censorString = "censored-by-test";
- var advancedSettings = new AdvancedSettings
- {
- Censors = new Censors(censorString).CensorBodyElementsByKeys(new List { "Table" })
- };
-
- // record cassette with advanced settings first
- var client = HttpClients.NewHttpClient(cassette, Mode.Record, advancedSettings);
- var fakeDataService = new FakeXmlDataService(client);
- var _ = await fakeDataService.GetIPAddressDataRawResponse();
-
- // now replay cassette
- client = HttpClients.NewHttpClient(cassette, Mode.Replay, advancedSettings);
- fakeDataService = new FakeXmlDataService(client);
- _ = await fakeDataService.GetIPAddressDataRawResponse();
-
- // TODO: Test is failing because the response is not being censored.
- // have to manually check cassette for the censored string in the response body
- }
-
- private static async Task GetIPAddressDataRequest(Cassette cassette, Mode mode)
+ private static async Task GetJsonDataRequest(Cassette cassette, Mode mode)
{
var client = HttpClients.NewHttpClient(cassette, mode, new AdvancedSettings
{
- MatchRules = MatchRules.DefaultStrict
+ MatchRules = MatchRules.DefaultStrict,
});
- var fakeDataService = new FakeJsonDataService(client);
+ var fakeDataService = new FakeDataService(client);
- return await fakeDataService.GetIPAddressData();
+ return await fakeDataService.GetJsonData();
}
}
}
diff --git a/EasyVCR.Tests/EasyVCR.Tests.csproj b/EasyVCR.Tests/EasyVCR.Tests.csproj
index 3f5edb0..5ae0560 100644
--- a/EasyVCR.Tests/EasyVCR.Tests.csproj
+++ b/EasyVCR.Tests/EasyVCR.Tests.csproj
@@ -3,7 +3,7 @@
net462;netstandard2.0;netcoreapp3.1;net5.0;net6.0;net7.0
- 8
+ 9Library
diff --git a/EasyVCR.Tests/FakeDataService.cs b/EasyVCR.Tests/FakeDataService.cs
index 0248265..1a31df6 100644
--- a/EasyVCR.Tests/FakeDataService.cs
+++ b/EasyVCR.Tests/FakeDataService.cs
@@ -17,10 +17,9 @@ public class IPAddressData
#endregion
}
- public abstract class FakeDataService
+ public class FakeDataService
{
private readonly EasyVCRHttpClient? _client;
- private readonly string? _format;
private readonly VCR? _vcr;
public EasyVCRHttpClient Client
@@ -35,39 +34,69 @@ public EasyVCRHttpClient Client
}
}
- protected FakeDataService(string format, VCR vcr)
+ public FakeDataService(VCR vcr)
{
- _format = format;
_vcr = vcr;
}
- protected FakeDataService(string format, EasyVCRHttpClient client)
+ public FakeDataService(EasyVCRHttpClient client)
{
- _format = format;
_client = client;
}
- public async Task GetIPAddressData()
+ public static string JsonDataUrl => "https://www.reddit.com/r/ProgrammerHumor.json";
+
+ public static string XmlDataUrl => "https://www.reddit.com/r/ProgrammerHumor.rss";
+
+ public static string HtmlDataUrl => "https://www.reddit.com/r/ProgrammerHumor";
+
+ public static string RawDataUrl => "https://raw.githubusercontent.com/nwithan8/UGAArchive/main/README.md";
+
+ public async Task GetJsonDataRawResponse()
{
- var response = await GetIPAddressDataRawResponse();
- return Convert(await response.Content.ReadAsStringAsync());
+ Client.DefaultRequestHeaders.Add("User-Agent", "EasyVCR"); // reddit requires a user agent
+ return await Client.GetAsync(JsonDataUrl);
}
- public async Task GetIPAddressDataRawResponse()
+ public async Task GetJsonData()
{
- return await Client.GetAsync(GetPreparedIPAddressDataUrl(_format));
+ var response = await GetJsonDataRawResponse();
+ return await response.Content.ReadAsStringAsync();
}
- protected abstract IPAddressData Convert(string responseBody);
+ public async Task GetXmlDataRawResponse()
+ {
+ Client.DefaultRequestHeaders.Add("User-Agent", "EasyVCR"); // reddit requires a user agent
+ return await Client.GetAsync(XmlDataUrl);
+ }
+
+ public async Task GetXmlData()
+ {
+ var response = await GetXmlDataRawResponse();
+ return await response.Content.ReadAsStringAsync();
+ }
+
+ public async Task GetHtmlDataRawResponse()
+ {
+ Client.DefaultRequestHeaders.Add("User-Agent", "EasyVCR"); // reddit requires a user agent
+ return await Client.GetAsync(HtmlDataUrl);
+ }
+
+ public async Task GetHtmlData()
+ {
+ var response = await GetHtmlDataRawResponse();
+ return await response.Content.ReadAsStringAsync();
+ }
- public static string GetPreparedIPAddressDataUrl(string? format)
+ public async Task GetRawDataRawResponse()
{
- return $"{GetIPAddressDataUrl()}?format={format}";
+ return await Client.GetAsync(RawDataUrl);
}
- public static string GetIPAddressDataUrl()
+ public async Task GetRawData()
{
- return "https://api.ipify.org/";
+ var response = await GetRawDataRawResponse();
+ return await response.Content.ReadAsStringAsync();
}
}
}
diff --git a/EasyVCR.Tests/FakeJsonDataServiceImpl.cs b/EasyVCR.Tests/FakeJsonDataServiceImpl.cs
deleted file mode 100644
index b8886e2..0000000
--- a/EasyVCR.Tests/FakeJsonDataServiceImpl.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System.Net.Http;
-
-// ReSharper disable UnusedMember.Global
-
-namespace EasyVCR.Tests
-{
- public class FakeJsonDataService : FakeDataService
- {
- public FakeJsonDataService(VCR vcr) : base("json", vcr)
- {
- }
-
- public FakeJsonDataService(EasyVCRHttpClient client) : base("json", client)
- {
- }
-
- protected override IPAddressData Convert(string responseBody)
- {
- return InternalUtilities.JSON.Serialization.ConvertJsonToObject(responseBody);
- }
- }
-
- public class FakeXmlDataService : FakeDataService
- {
- public FakeXmlDataService(VCR vcr) : base("xml", vcr)
- {
- }
-
- public FakeXmlDataService(EasyVCRHttpClient client) : base("xml", client)
- {
- }
-
- protected override IPAddressData Convert(string responseBody)
- {
- return InternalUtilities.XML.Serialization.ConvertXmlToObject(responseBody);
- }
- }
-}
diff --git a/EasyVCR.Tests/Sample.cs b/EasyVCR.Tests/Sample.cs
index 6340db4..d07c367 100644
--- a/EasyVCR.Tests/Sample.cs
+++ b/EasyVCR.Tests/Sample.cs
@@ -43,14 +43,22 @@ public async Task AdvancedVCRExample()
{
var bodyElementsToIgnoreDuringMatch = new List
{
- new CensorElement("name", true),
- new CensorElement("phone", false)
+ new KeyCensorElement("name", true),
+ new KeyCensorElement("phone", false),
+ };
+ var headerCensors = new List
+ {
+ new("X-My-Header", true),
+ };
+ var queryParameterCensors = new List
+ {
+ new("api_key", false),
};
var advancedSettings = new AdvancedSettings
{
MatchRules = new MatchRules().ByBody(bodyElementsToIgnoreDuringMatch).ByHeader("X-My-Header"), // Match recorded requests by body and a specific header
- Censors = new Censors("redacted").CensorHeadersByKeys(new List { "Header-To-Hide" }).CensorQueryParametersByKeys(new List { "api_key" }), // Redact a specific header and query parameter
- ManualDelay = 1000 // Simulate a delay of 1 second
+ Censors = new Censors("redacted").CensorHeaders(headerCensors).CensorQueryParameters(queryParameterCensors), // Redact a specific header and query parameter
+ ManualDelay = 1000, // Simulate a delay of 1 second
};
var order = new CassetteOrder.None(); // elements of each request in a cassette will not be ordered any particular way
var vcr = new VCR(advancedSettings);
diff --git a/EasyVCR.Tests/VCRTest.cs b/EasyVCR.Tests/VCRTest.cs
index 4b00f11..db7df96 100644
--- a/EasyVCR.Tests/VCRTest.cs
+++ b/EasyVCR.Tests/VCRTest.cs
@@ -18,9 +18,13 @@ public async Task TestAdvancedSettings()
// refer to ClientTest.cs for individual per-settings tests
const string censorString = "censored-by-test";
+ var headerCensors = new List
+ {
+ new("Date", true),
+ };
var advancedSettings = new AdvancedSettings
{
- Censors = new Censors(censorString).CensorHeadersByKeys(new List { "Date" })
+ Censors = new Censors(censorString).CensorHeaders(headerCensors),
};
var vcr = new VCR(advancedSettings);
@@ -36,16 +40,16 @@ public async Task TestAdvancedSettings()
// record first
vcr.Record();
var client = vcr.Client;
- var fakeDataService = new FakeJsonDataService(client);
- var _ = await fakeDataService.GetIPAddressData();
+ var fakeDataService = new FakeDataService(client);
+ var _ = await fakeDataService.GetJsonDataRawResponse();
// now replay and confirm that the censor is applied
vcr.Replay();
// changing the VCR settings won't affect a client after it's been grabbed from the VCR
// so, we need to re-grab the VCR client and re-create the FakeDataService
client = vcr.Client;
- fakeDataService = new FakeJsonDataService(client);
- var response = await fakeDataService.GetIPAddressDataRawResponse();
+ fakeDataService = new FakeDataService(client);
+ var response = await fakeDataService.GetJsonDataRawResponse();
Assert.IsNotNull(response);
Assert.IsTrue(response.Headers.Contains("Date"));
var censoredHeader = response.Headers.GetValues("Date").FirstOrDefault();
@@ -105,7 +109,7 @@ public void TestClientHandOff()
vcr.Insert(cassette);
// test that we can still control the VCR even after it's been handed off to the service using it
- var fakeDataService = new FakeJsonDataService(vcr);
+ var fakeDataService = new FakeDataService(vcr);
// Client should come from VCR, which has a client because it has a cassette.
Assert.IsNotNull(fakeDataService.Client);
@@ -142,9 +146,9 @@ public async Task TestErase()
vcr.Insert(cassette);
// record a request to a cassette
- var fakeDataService = new FakeJsonDataService(vcr);
- var summary = await fakeDataService.GetIPAddressData();
- Assert.IsNotNull(summary);
+ var fakeDataService = new FakeDataService(vcr);
+ var responseBody = await fakeDataService.GetJsonData();
+ Assert.IsNotNull(responseBody);
Assert.IsTrue(cassette.NumInteractions > 0);
// erase the cassette
@@ -183,11 +187,10 @@ public async Task TestRecord()
var cassette = TestUtils.GetCassette("test_vcr_record");
var vcr = TestUtils.GetSimpleVCR(Mode.Record);
vcr.Insert(cassette);
- var fakeDataService = new FakeJsonDataService(vcr);
+ var fakeDataService = new FakeDataService(vcr);
- var summary = await fakeDataService.GetIPAddressData();
- Assert.IsNotNull(summary);
- Assert.IsNotNull(summary.IPAddress);
+ var responseBody = await fakeDataService.GetJsonData();
+ Assert.IsNotNull(responseBody);
Assert.IsTrue(cassette.NumInteractions > 0);
}
@@ -197,21 +200,21 @@ public async Task TestReplay()
var cassette = TestUtils.GetCassette("test_vcr_replay");
var vcr = TestUtils.GetSimpleVCR(Mode.Record);
vcr.Insert(cassette);
- var fakeDataService = new FakeJsonDataService(vcr);
+ var fakeDataService = new FakeDataService(vcr);
// record first
- var _ = await fakeDataService.GetIPAddressData();
+ var _ = await fakeDataService.GetJsonDataRawResponse();
Assert.IsTrue(cassette.NumInteractions > 0); // make sure we recorded something
// now replay
vcr.Replay();
- var summary = await fakeDataService.GetIPAddressData();
- Assert.IsNotNull(summary);
+ var responseBody = await fakeDataService.GetJsonData();
+ Assert.IsNotNull(responseBody);
// double check by erasing the cassette and trying to replay
vcr.Erase();
// should throw an exception because there's no matching interaction now
- await Assert.ThrowsExceptionAsync(async () => await fakeDataService.GetIPAddressData());
+ await Assert.ThrowsExceptionAsync(async () => await fakeDataService.GetJsonDataRawResponse());
}
[TestMethod]
@@ -220,11 +223,10 @@ public async Task TestRequest()
var cassette = TestUtils.GetCassette("test_vcr_record");
var vcr = TestUtils.GetSimpleVCR(Mode.Bypass);
vcr.Insert(cassette);
- var fakeDataService = new FakeJsonDataService(vcr);
+ var fakeDataService = new FakeDataService(vcr);
- var summary = await fakeDataService.GetIPAddressData();
- Assert.IsNotNull(summary);
- Assert.IsNotNull(summary.IPAddress);
+ var responseBody = await fakeDataService.GetJsonData();
+ Assert.IsNotNull(responseBody);
}
}
}
diff --git a/EasyVCR/CensorElement.cs b/EasyVCR/CensorElement.cs
index fdc0bc1..b619147 100644
--- a/EasyVCR/CensorElement.cs
+++ b/EasyVCR/CensorElement.cs
@@ -3,23 +3,27 @@
namespace EasyVCR
{
- public class CensorElement
+ ///
+ /// The base class for censor elements.
+ ///
+ public abstract class CensorElement
{
///
/// Whether the name is case-sensitive.
///
protected bool CaseSensitive { get; }
+
///
- /// Name of the element to censor.
+ /// Value to look for.
///
protected string Value { get; }
///
/// Constructor for a new censor element.
///
- /// Name of the element to censor.
- /// Whether the name is case-sensitive.
- public CensorElement(string value, bool caseSensitive)
+ /// Value to censor.
+ /// Whether the value is case-sensitive.
+ protected CensorElement(string value, bool caseSensitive)
{
Value = value;
CaseSensitive = caseSensitive;
@@ -28,14 +32,81 @@ public CensorElement(string value, bool caseSensitive)
///
/// Checks whether the provided element matches this censor element, accounting for case sensitivity.
///
- /// The name to check.
+ /// The value to check.
+ /// The key to check.
+ /// True if the element matches, false otherwise.
+ internal abstract bool Matches(string value, string? key = null);
+ }
+
+ ///
+ /// A censor element, used to define a raw value that should be censored.
+ ///
+ public class TextCensorElement : CensorElement
+ {
+ ///
+ /// Constructor for a new text censor element.
+ ///
+ /// The raw text value of the element to censor.
+ /// Whether the value is case-sensitive.
+ public TextCensorElement(string value, bool caseSensitive) : base(value, caseSensitive)
+ {
+ }
+
+ ///
+ /// Checks whether the provided element matches this censor element, accounting for case sensitivity.
+ ///
+ /// The value to check.
+ /// The key to check.
+ /// True if the element matches, false otherwise.
+ internal override bool Matches(string value, string? key = null)
+ {
+ // we only care about the value here
+ return CaseSensitive ? Value.Equals(value) : Value.Equals(value, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Replace the provided value with the provided replacement if it matches this censor element.
+ /// Otherwise, return the value as-is.
+ ///
+ /// Value to replace.
+ /// Replacement for the value.
+ /// The value with the replacement inserted if it matches this censor element, otherwise the value as-is.
+ internal string MatchAndReplaceAsNeeded(string value, string replacement)
+ {
+ return Matches(value) ? replacement : value;
+ }
+ }
+
+ ///
+ /// A censor element, used to define a key-value pair that should be censored.
+ ///
+ public class KeyCensorElement : CensorElement
+ {
+ ///
+ /// Constructor for a new key censor element.
+ ///
+ /// Key of which to censor the corresponding value.
+ /// Whether the key is case-sensitive.
+ public KeyCensorElement(string key, bool caseSensitive) : base(key, caseSensitive)
+ {
+ }
+
+ ///
+ /// Checks whether the provided element matches this censor element, accounting for case sensitivity.
+ ///
+ /// The value to check.
+ /// The key to check.
/// True if the element matches, false otherwise.
- internal virtual bool Matches(string key)
+ internal override bool Matches(string value, string? key = null)
{
+ // we only care about the key here
return CaseSensitive ? Value.Equals(key) : Value.Equals(key, StringComparison.OrdinalIgnoreCase);
}
}
+ ///
+ /// A censor element, used to define a regex pattern that should be censored.
+ ///
public class RegexCensorElement : CensorElement
{
///
@@ -75,9 +146,11 @@ internal string MatchAndReplaceAsNeeded(string value, string replacement)
/// Checks whether the provided element matches this censor element, accounting for case sensitivity.
///
/// The value to check.
+ /// The key to check.
/// True if the element matches, false otherwise.
- internal override bool Matches(string value)
+ internal override bool Matches(string value, string? key = null)
{
+ // we only care about the value here
var options = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture | RegexOptions.Singleline;
if (!CaseSensitive)
{
diff --git a/EasyVCR/Censors.cs b/EasyVCR/Censors.cs
index c7658ea..6af0992 100644
--- a/EasyVCR/Censors.cs
+++ b/EasyVCR/Censors.cs
@@ -36,9 +36,15 @@ public static Censors DefaultSensitive
get
{
var censors = new Censors();
- censors.CensorHeadersByKeys(Defaults.CredentialHeadersToHide);
- censors.CensorQueryParametersByKeys(Defaults.CredentialParametersToHide);
- censors.CensorBodyElementsByKeys(Defaults.CredentialParametersToHide);
+
+ var headerCensors = Defaults.CredentialHeadersToHide.Select(header => new KeyCensorElement(header, false)).ToList();
+ censors.CensorHeaders(headerCensors);
+
+ var queryParamCensors = Defaults.CredentialParametersToHide.Select(queryParam => new KeyCensorElement(queryParam, false)).ToList();
+ censors.CensorQueryParameters(queryParamCensors);
+
+ var bodyElementCensors = Defaults.CredentialParametersToHide.Select(bodyElement => new KeyCensorElement(bodyElement, false)).ToList();
+ censors.CensorBodyElements(bodyElementCensors);
return censors;
}
@@ -60,7 +66,13 @@ public Censors(string? censorString = null)
///
/// Add a rule to censor specified body elements.
///
- /// List of body elements to censor.
+ ///
+ /// List of body elements to censor.
+ /// JSON: Use or . Any will be ignored.
+ /// XML: Use or . Any will be ignored.
+ /// HTML: Use or . Any will be ignored.
+ /// Plain text: Use or . Any will be ignored.
+ ///
/// The current Censor object.
public Censors CensorBodyElements(IEnumerable elements)
{
@@ -68,78 +80,29 @@ public Censors CensorBodyElements(IEnumerable elements)
return this;
}
- ///
- /// Add a rule to censor specified body elements by their keys.
- ///
- /// List of keys of body elements to censor.
- /// Whether to match case sensitively.
- ///
- public Censors CensorBodyElementsByKeys(List elementKeys, bool caseSensitive = false)
- {
- foreach (var key in elementKeys)
- {
- _bodyElementsToCensor.Add(new CensorElement(key, caseSensitive));
- }
-
- return this;
- }
-
///
/// Add a rule to censor specified headers.
/// Note: This will censor the header keys in both the request and response.
///
/// List of headers to censor.
/// The current Censor object.
- public Censors CensorHeaders(IEnumerable headers)
+ public Censors CensorHeaders(IEnumerable headers)
{
_headersToCensor.AddRange(headers);
return this;
}
- ///
- /// Add a rule to censor specified headers by their keys.
- /// Note: This will censor the header keys in both the request and response.
- ///
- /// List of keys of header to censor.
- /// Whether to match case sensitively.
- /// The current Censor object.
- public Censors CensorHeadersByKeys(List headerKeys, bool caseSensitive = false)
- {
- foreach (var key in headerKeys)
- {
- _headersToCensor.Add(new CensorElement(key, caseSensitive));
- }
-
- return this;
- }
-
///
/// Add a rule to censor specified query parameters.
///
/// List of query parameters to censor.
/// The current Censor object.
- public Censors CensorQueryParameters(IEnumerable elements)
+ public Censors CensorQueryParameters(IEnumerable elements)
{
_queryParamsToCensor.AddRange(elements);
return this;
}
- ///
- /// Add a rule to censor specified query parameters by their keys.
- ///
- /// List of keys of query parameters to censor.
- /// Whether to match case sensitively.
- /// The current Censor object.
- public Censors CensorQueryParametersByKeys(List parameterKeys, bool caseSensitive = false)
- {
- foreach (var key in parameterKeys)
- {
- _queryParamsToCensor.Add(new CensorElement(key, caseSensitive));
- }
-
- return this;
- }
-
///
/// Add a rule to censor specified path elements.
///
@@ -151,22 +114,6 @@ public Censors CensorPathElements(IEnumerable elements)
return this;
}
- ///
- /// Add a rule to censor specified path elements by their patterns.
- ///
- /// List of patterns of path elements to censor.
- /// Whether to match case sensitively.
- /// The current Censor object.
- public Censors CensorPathElementsByPatterns(List patterns, bool caseSensitive = false)
- {
- foreach (var pattern in patterns)
- {
- _pathElementsToCensor.Add(new RegexCensorElement(pattern, caseSensitive));
- }
-
- return this;
- }
-
///
/// Censor the appropriate body parameters.
///
@@ -196,9 +143,9 @@ internal string ApplyBodyParametersCensors(string body, ContentType? contentType
{
case ContentType.Text:
case ContentType.Html:
- return body; // We can't censor plaintext bodies or HTML bodies.
+ return CensorTextData(body, _censorText, _bodyElementsToCensor);
case ContentType.Xml:
- return body; // XML parsing is not supported yet, so we can't censor XML bodies.
+ return CensorXmlData(body, _censorText, _bodyElementsToCensor);
case ContentType.Json:
return CensorJsonData(body, _censorText, _bodyElementsToCensor);
default:
@@ -225,7 +172,7 @@ internal IDictionary ApplyHeaderCensors(IDictionary header.Key, header => ElementShouldBeCensored(header.Key, _headersToCensor) ? _censorText : header.Value);
+ return _headersToCensor.Count == 0 ? headers : headers.ToDictionary(header => header.Key, header => ElementShouldBeCensored(foundValue: header.Value, foundKey: header.Key, _headersToCensor) ? _censorText : header.Value);
}
///
@@ -293,6 +240,7 @@ internal IDictionary ApplyHeaderCensors(IDictionary ApplyHeaderCensors(IDictionary ApplyHeaderCensors(IDictionary
+ /// Apply censors to a raw text string.
+ ///
+ /// Raw text string to apply censors to.
+ /// Text to use to replace censored elements.
+ /// List of elements to censor.
+ /// A censored raw text string.
+ public static string CensorTextData(string data, string censorText, IReadOnlyCollection elementsToCensor)
+ {
+ if (elementsToCensor.Count == 0)
+ {
+ // short circuit if there are no censors to apply
+ return data;
+ }
+
+ var censoredData = data;
+ foreach (var censorElement in elementsToCensor)
+ {
+ switch (censorElement)
+ {
+ // we cannot process KeyCensorElements in raw text/html
+ case KeyCensorElement _:
+ continue;
+ case RegexCensorElement regexCensorElement:
+ censoredData = regexCensorElement.MatchAndReplaceAsNeeded(censoredData, censorText);
+ break;
+ case TextCensorElement textCensorElement:
+ censoredData = textCensorElement.MatchAndReplaceAsNeeded(censoredData, censorText);
+ break;
+ }
+ }
+
+ return censoredData;
+ }
+
///
/// Apply censors to a JSON string.
///
/// JSON string to apply censors to.
- /// Test to use to replace censored elements.
+ /// Text to use to replace censored elements.
/// List of elements to censor.
/// A censored JSON string.
public static string CensorJsonData(string data, string censorText, IReadOnlyCollection elementsToCensors)
@@ -368,7 +353,7 @@ public static string CensorJsonData(string data, string censorText, IReadOnlyCol
try
{
var jsonDictionary = JsonSerialization.ConvertJsonToObject>(data);
- var censoredJsonDictionary = ApplyDataCensors(jsonDictionary, censorText, elementsToCensors);
+ var censoredJsonDictionary = ApplyJsonXmlDataCensors(jsonDictionary, censorText, elementsToCensors);
return JsonSerialization.ConvertObjectToJson(censoredJsonDictionary);
}
catch (Exception)
@@ -377,7 +362,7 @@ public static string CensorJsonData(string data, string censorText, IReadOnlyCol
try
{
var jsonList = JsonSerialization.ConvertJsonToObject>(data);
- var censoredJsonList = ApplyDataCensors(jsonList, censorText, elementsToCensors);
+ var censoredJsonList = ApplyJsonXmlDataCensors(jsonList, censorText, elementsToCensors);
return JsonSerialization.ConvertObjectToJson(censoredJsonList);
}
catch
@@ -392,7 +377,7 @@ public static string CensorJsonData(string data, string censorText, IReadOnlyCol
/// Apply censors to an XML string.
///
/// XML string to apply censors to.
- /// Test to use to replace censored elements.
+ /// Text to use to replace censored elements.
/// List of elements to censor.
/// A censored XML string.
public static string CensorXmlData(string data, string censorText, IReadOnlyCollection elementsToCensors)
@@ -400,7 +385,7 @@ public static string CensorXmlData(string data, string censorText, IReadOnlyColl
try
{
var xmlDictionary = XmlSerialization.ConvertXmlToObject>(data);
- var censoredXmlDictionary = ApplyDataCensors(xmlDictionary, censorText, elementsToCensors);
+ var censoredXmlDictionary = ApplyJsonXmlDataCensors(xmlDictionary, censorText, elementsToCensors);
return XmlSerialization.ConvertObjectToXml(censoredXmlDictionary);
}
catch (Exception)
@@ -409,7 +394,7 @@ public static string CensorXmlData(string data, string censorText, IReadOnlyColl
try
{
var xmlList = XmlSerialization.ConvertXmlToObject>(data);
- var censoredXmlList = ApplyDataCensors(xmlList, censorText, elementsToCensors);
+ var censoredXmlList = ApplyJsonXmlDataCensors(xmlList, censorText, elementsToCensors);
return XmlSerialization.ConvertObjectToXml(censoredXmlList);
}
catch
@@ -427,7 +412,7 @@ public static string CensorXmlData(string data, string censorText, IReadOnlyColl
/// Test to use to replace censored elements.
/// List of elements to censor.
/// A censored list of elements.
- private static List