write method
Writes recording, replacing whatever was there for the same exchange.
No request headers at all. They are not part of the key, so keeping
them would be writing an Authorization into a committed file for
nothing.
Response headers are kept except the ones keptHeader refuses, which
is a deny-list and deliberately not an allow-list. An allow-list is the
safer-sounding of the two and the wrong one here: what it drops it drops
silently, and the first thing it drops is Link — so a screen that
paginates works under live, is recorded, and quietly renders one page
forever after, with the recording looking correct in the diff. A
deny-list can only be wrong about a header nobody thought of, and it is
wrong about it in a file somebody reads before committing.
A response body is on the same footing: an endpoint that answers with a token answers with a token. That is true of any fixture, and there is nothing this can do about it beyond writing it somewhere reviewable.
Unchanged is untouched. When the store already holds the same
status, content type, kept headers and body under the current name,
nothing is written — so re-recording an API that did not move leaves
git status clean, and a reviewer reading the diff of a record run reads
only what moved. That is also why no file carries the time it was
fetched: git already knows when the bytes last changed, and a stamp
rewritten on every run would put every file in every diff.
Implementation
void write(ScenarioRecording recording) {
var name = _name(recording.method, recording.url);
var legacy = _legacyName(recording.method, recording.url);
if (_readAs(name, recording.method, recording.url) case var held?
when _sameExchange(held, recording)) {
return;
}
Directory(directory).createSync(recursive: true);
// A recording made before names kept the end of the path is moved to the
// current name rather than left beside it.
if (legacy != name) {
var file = File(p.join(directory, '$legacy.json'));
if (file.existsSync()) file.deleteSync();
_deleteBodies(legacy);
}
// Unconditionally, and before the new one is written: a body recorded as
// JSON and re-recorded as an error page changes extension, and the old
// file would otherwise sit in the store forever with nothing left naming
// it.
_deleteBodies(name);
var body = recording.body.isEmpty
? null
: '$name.body.${_extensionFor(recording.contentType)}';
if (body != null) {
File(p.join(directory, body)).writeAsBytesSync(recording.body);
}
File(p.join(directory, '$name.json')).writeAsStringSync(
'${const JsonEncoder.withIndent(' ').convert({
'version': 1,
'method': recording.method,
'url': '${recording.url}',
'status': recording.status,
'contentType': ?recording.contentType,
if (recording.headers.isNotEmpty) 'headers': {for (var key in recording.headers.keys.toList()..sort()) key: recording.headers[key]},
'bytes': recording.body.length,
'body': ?body,
})}\n',
);
}