Skip to content

Commit

Permalink
- Add docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
nwithan8 committed Jan 31, 2023
1 parent 8cd08f4 commit 4ee81df
Show file tree
Hide file tree
Showing 12 changed files with 140 additions and 26 deletions.
9 changes: 9 additions & 0 deletions lib/src/advanced_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,28 @@ import 'package:dartvcr/src/time_frame.dart';
import 'censors.dart';
import 'expiration_actions.dart';

/// A collection of configuration options that can be used when recording and replaying requests.
class AdvancedOptions {
/// A collection of censor rules that will be applied to the request and response bodies.
final Censors censors;

/// The rules that will be used to match requests to recorded requests.
final MatchRules matchRules;

/// The number of milliseconds to delay before returning a response.
final int manualDelay;

/// If true, a replayed request will be delayed by the same amount of time it took to make the original live request.
final bool simulateDelay;

/// The time frame during which a request can be replayed.
/// If the request is replayed outside of this time frame, the [whenExpired] action will be taken.
final TimeFrame validTimeFrame;

/// The action to take when a request is replayed outside of the [validTimeFrame].
final ExpirationAction whenExpired;

/// Creates a new [AdvancedOptions].
AdvancedOptions(
{Censors? censors,
MatchRules? matchRules,
Expand Down
17 changes: 15 additions & 2 deletions lib/src/cassette.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@ import 'dart:io';

import 'request_elements/http_interaction.dart';

/// A class representing a cassette that contains a list of [HttpInteraction]s.
class Cassette {
/// The path to the cassette file.
final String _filePath;

/// The name of the cassette.
final String name;

/// Whether or not the cassette is locked (i.e. being written to).
bool _locked = false;

Cassette(folderPath, this.name)
: _filePath = '$folderPath/$name.json';
/// Creates a new [Cassette] with the given [name] and [folderPath].
Cassette(folderPath, this.name) : _filePath = '$folderPath/$name.json';

/// Returns the number of interactions in the cassette.
int get numberOfInteractions => read().length;

/// Returns a list of all [HttpInteraction]s in the cassette.
List<HttpInteraction> read() {
List<HttpInteraction> interactions = [];

Expand All @@ -33,6 +40,7 @@ class Cassette {
return interactions;
}

/// Adds or overwrites the given [interaction] to the cassette.
void update(HttpInteraction interaction) {
File file = File(_filePath);
if (!file.existsSync()) {
Expand All @@ -48,26 +56,31 @@ class Cassette {
File(_filePath).writeAsStringSync(jsonEncode(interactions));
}

/// Deletes the cassette file.
void erase() {
File file = File(_filePath);
if (file.existsSync()) {
file.deleteSync();
}
}

/// Returns true if the cassette file exists.
bool _exists() {
File file = File(_filePath);
return file.existsSync();
}

/// Locks the cassette so that it cannot be written to.
void lock() {
_locked = true;
}

/// Unlocks the cassette so that it can be written to.
void unlock() {
_locked = false;
}

/// Check if the cassette can be written to.
void _preWriteCheck() {
if (_locked) {
throw Exception('Cassette $name is locked');
Expand Down
5 changes: 5 additions & 0 deletions lib/src/censor_element.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
/// A censor element is a single element to censor from requests and responses.
class CensorElement {
/// The name or key of the element to censor.
final String name;

/// Whether or not the element should be censored only when matching case exactly.
final bool caseSensitive;

/// Creates a new [CensorElement] with the given [name] and [caseSensitive] flag.
CensorElement(this.name, {this.caseSensitive = false});

/// Returns true if the given [key] matches this element.
bool matches(String key) {
if (caseSensitive) {
return key == name;
Expand Down
41 changes: 32 additions & 9 deletions lib/src/censors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,73 @@ import 'package:dartvcr/src/vcr_exception.dart';
import 'censor_element.dart';
import 'internal_utilities/content_type.dart';

/// A class representing a set of rules to censor elements from requests and responses.
class Censors {
/// The string to replace censored elements with.
final String _censorString;

/// The list of elements to censor from request bodies.
final List<CensorElement> _bodyElementsToCensor;

/// The list of elements to censor from request headers.
final List<CensorElement> _headerElementsToCensor;

/// The list of elements to censor from request query parameters.
final List<CensorElement> _queryElementsToCensor;

/// Creates a new [Censors] with the given [censorString].
Censors({String censorString = "******"})
: _censorString = censorString,
_bodyElementsToCensor = [],
_headerElementsToCensor = [],
_queryElementsToCensor = [];

/// Add the given [element] to the list of elements to censor from request bodies.
Censors censorBodyElements(List<CensorElement> elements) {
_bodyElementsToCensor.addAll(elements);
return this;
}

/// Add the given [keys] to the list of elements to censor from request bodies.
/// If [caseSensitive] is true, the keys will be censored only when matching case exactly.
Censors censorBodyElementsByKeys(List<String> keys,
{bool caseSensitive = false}) {
_bodyElementsToCensor
.addAll(keys.map((key) => CensorElement(key, caseSensitive: caseSensitive)));
_bodyElementsToCensor.addAll(
keys.map((key) => CensorElement(key, caseSensitive: caseSensitive)));
return this;
}

/// Add the given [element] to the list of elements to censor from request headers.
Censors censorHeaderElements(List<CensorElement> elements) {
_headerElementsToCensor.addAll(elements);
return this;
}

/// Add the given [keys] to the list of elements to censor from request headers.
/// If [caseSensitive] is true, the keys will be censored only when matching case exactly.
Censors censorHeaderElementsByKeys(List<String> keys,
{bool caseSensitive = false}) {
_headerElementsToCensor
.addAll(keys.map((key) => CensorElement(key, caseSensitive: caseSensitive)));
_headerElementsToCensor.addAll(
keys.map((key) => CensorElement(key, caseSensitive: caseSensitive)));
return this;
}

/// Add the given [element] to the list of elements to censor from request query parameters.
Censors censorQueryElements(List<CensorElement> elements) {
_queryElementsToCensor.addAll(elements);
return this;
}

/// Add the given [keys] to the list of elements to censor from request query parameters.
/// If [caseSensitive] is true, the keys will be censored only when matching case exactly.
Censors censorQueryElementsByKeys(List<String> keys,
{bool caseSensitive = false}) {
_queryElementsToCensor
.addAll(keys.map((key) => CensorElement(key, caseSensitive: caseSensitive)));
_queryElementsToCensor.addAll(
keys.map((key) => CensorElement(key, caseSensitive: caseSensitive)));
return this;
}

/// Applies the body parameter censors to the given [body] with the given [contentType].
// TODO: Only works on JSON bodies
String applyBodyParameterCensors(String body, ContentType contentType) {
if (body.isEmpty) {
Expand Down Expand Up @@ -87,6 +103,7 @@ class Censors {
}
}

/// Removes all null values from the given [map].
Map<String, String> _removeNullValuesFromMap(Map<String, String?> map) {
Map<String, String> result = {};
map.forEach((key, value) {
Expand All @@ -97,6 +114,7 @@ class Censors {
return result;
}

/// Applies the header censors to the given [headers].
Map<String, String> applyHeaderCensors(Map<String, String?>? headers) {
if (headers == null || headers.isEmpty) {
// short circuit if headers is empty
Expand All @@ -112,7 +130,6 @@ class Censors {

headers.forEach((key, value) {
if (value == null) {

} else if (elementShouldBeCensored(key, _headerElementsToCensor)) {
newHeaders[key] = _censorString;
} else {
Expand All @@ -123,6 +140,7 @@ class Censors {
return newHeaders;
}

/// Applies the query parameter censors to the given [url].
String applyQueryCensors(String url) {
if (_queryElementsToCensor.isEmpty) {
// short circuit if there are no censors to apply
Expand Down Expand Up @@ -151,6 +169,7 @@ class Censors {
).toString();
}

/// Censors the given [elementsToCensor] in the given [body] with the given [censorString].
static String censorJsonData(
String body, String censorString, List<CensorElement> elementsToCensor) {
try {
Expand All @@ -172,8 +191,9 @@ class Censors {
}
}

static Map<String, dynamic> censorMap(Map<String, dynamic> map, String censorString,
List<CensorElement> elementsToCensor) {
/// Censors the given [elementsToCensor] in the given [map] with the given [censorString].
static Map<String, dynamic> censorMap(Map<String, dynamic> map,
String censorString, List<CensorElement> elementsToCensor) {
if (elementsToCensor.isEmpty) {
// short circuit if there are no censors to apply
return map;
Expand Down Expand Up @@ -216,6 +236,7 @@ class Censors {
return censoredMap;
}

/// Censors the given [elementsToCensor] in the given [list] with the given [censorString].
static List<dynamic> censorList(List<dynamic> list, String censorString,
List<CensorElement> elementsToCensor) {
if (elementsToCensor.isEmpty) {
Expand All @@ -241,12 +262,14 @@ class Censors {
return censoredList;
}

/// Returns true if the given [foundKey] exists in the given [elementsToCensor] list.
static bool elementShouldBeCensored(
String foundKey, List<CensorElement> elementsToCensor) {
return elementsToCensor.isNotEmpty &&
elementsToCensor.any((element) => element.matches(foundKey));
}

/// A pre-configured instance of [Censors] that censors nothing.
static Censors get defaultCensors {
return Censors();
}
Expand Down
20 changes: 16 additions & 4 deletions lib/src/dartvcr_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,30 @@ import 'package:http/http.dart' as http;
import 'cassette.dart';
import 'mode.dart';

/// A [http.BaseClient] that records and replays HTTP interactions.
class DartVCRClient extends http.BaseClient {
/// The internal [http.Client] that will be used to make requests.
final http.Client _client;

/// The [Cassette] the client will use during requests.
final Cassette _cassette;

/// The [Mode] the client will use during requests.
final Mode _mode;

/// The [AdvancedOptions] the client will use during requests.
final AdvancedOptions _advancedOptions;

DartVCRClient(this._cassette, this._mode,
{AdvancedOptions? advancedOptions})
/// Creates a new [DartVCRClient] with the given [Cassette], [Mode] and [AdvancedOptions].
DartVCRClient(this._cassette, this._mode, {AdvancedOptions? advancedOptions})
: _client = http.Client(),
_advancedOptions = advancedOptions ?? AdvancedOptions();

/// Simulates an HTTP request and response.
/// Makes a real request and records the response if the [Mode] is [Mode.record].
/// Drops the request and returns a recorded response if the [Mode] is [Mode.replay].
/// Makes a real request and returns the real response if the [Mode] is [Mode.bypass].
/// Either makes a real request and records the response, or drops the request and returns a recorded response if the [Mode] is [Mode.auto].
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
switch (_mode) {
Expand Down Expand Up @@ -87,8 +97,7 @@ class DartVCRClient extends http.BaseClient {
// simulate delay if configured
await _simulateDelay(replayInteraction);
// return matching interaction's response
return replayInteraction
.toStreamedResponse(_advancedOptions.censors);
return replayInteraction.toStreamedResponse(_advancedOptions.censors);
}

// no matching interaction found, make real request, record response
Expand All @@ -100,6 +109,7 @@ class DartVCRClient extends http.BaseClient {
}
}

/// Finds a matching recorded [HttpInteraction] for the given [http.BaseRequest].
HttpInteraction? _findMatchingInteraction(http.BaseRequest request) {
List<HttpInteraction> interactions = _cassette.read();

Expand All @@ -115,6 +125,7 @@ class DartVCRClient extends http.BaseClient {
}
}

/// Makes a real request and records the response.
Future<http.StreamedResponse> _recordRequestAndResponse(
http.BaseRequest request) async {
Stopwatch stopwatch = Stopwatch();
Expand All @@ -135,6 +146,7 @@ class DartVCRClient extends http.BaseClient {
return Response.toStream(response, _advancedOptions.censors);
}

/// Simulates an HTTP delay if configured to do so.
Future _simulateDelay(HttpInteraction interaction) async {
int delay = 0;
if (_advancedOptions.simulateDelay == true) {
Expand Down
3 changes: 3 additions & 0 deletions lib/src/defaults.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
const String viaRecordingHeaderKey = "X-Via-DartVCR-Recording";

/// A set of default headers that will be added to all requests replayed by the VCR.
Map<String, String> get replayHeaders => {
viaRecordingHeaderKey: "true",
};

/// A set of common credential headers that will be hidden from recordings.
List<String> get credentialHeadersToHide => [
"authorization",
"cookie",
];

/// A set of common credential parameters that will be hidden from recordings.
List<String> get credentialParametersToHide => [
"access_token",
"client_id",
Expand Down
4 changes: 4 additions & 0 deletions lib/src/expiration_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import 'package:dartvcr/src/vcr_exception.dart';

import 'mode.dart';

/// The various actions that can be taken when an interaction expires.
enum ExpirationAction {
/// warn: The VCR will log a warning to the console.
warn,
/// throwException: The VCR will throw an exception.
throwException,
/// recordAgain: The VCR will silently re-record the interaction.
recordAgain,
}

Expand Down
Loading

0 comments on commit 4ee81df

Please sign in to comment.