Tests
This commit is contained in:
parent
205d654cca
commit
efab4d28e2
6 changed files with 137 additions and 16 deletions
|
@ -0,0 +1,8 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="sends 304 on if-modified-since in cache_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/test/cache_test.dart" />
|
||||||
|
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
|
||||||
|
<option name="testName" value="sends 304 on if-modified-since" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
6
.idea/runConfigurations/tests_in_cache_test_dart.xml
Normal file
6
.idea/runConfigurations/tests_in_cache_test_dart.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="tests in cache_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/test/cache_test.dart" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
|
@ -37,7 +37,7 @@ Future configureServer(Angel app) async {
|
||||||
cache.patterns.addAll([
|
cache.patterns.addAll([
|
||||||
'robots.txt',
|
'robots.txt',
|
||||||
new RegExp(r'\.(png|jpg|gif|txt)$'),
|
new RegExp(r'\.(png|jpg|gif|txt)$'),
|
||||||
new Glob('/public/**/*'),
|
new Glob('public/**/*'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// REQUIRED: The middleware that serves cached responses
|
// REQUIRED: The middleware that serves cached responses
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:pool/pool.dart';
|
import 'package:pool/pool.dart';
|
||||||
|
import 'util.dart';
|
||||||
final DateFormat _fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss');
|
|
||||||
|
|
||||||
/// Formats a date (converted to UTC), ex: `Sun, 03 May 2015 23:02:37 GMT`.
|
|
||||||
String _formatDateForHttp(DateTime dt) => _fmt.format(dt.toUtc()) + ' GMT';
|
|
||||||
|
|
||||||
/// A flexible response cache for Angel.
|
/// A flexible response cache for Angel.
|
||||||
///
|
///
|
||||||
|
@ -39,7 +34,7 @@ class ResponseCache {
|
||||||
/// This prevents the server from even having to access the cache, and plays very well with static assets.
|
/// This prevents the server from even having to access the cache, and plays very well with static assets.
|
||||||
Future<bool> ifModifiedSince(RequestContext req, ResponseContext res) async {
|
Future<bool> ifModifiedSince(RequestContext req, ResponseContext res) async {
|
||||||
if (req.headers.value('if-modified-since') != null) {
|
if (req.headers.value('if-modified-since') != null) {
|
||||||
var modifiedSince = _fmt
|
var modifiedSince = fmt
|
||||||
.parse(req.headers.value('if-modified-since').replaceAll('GMT', ''));
|
.parse(req.headers.value('if-modified-since').replaceAll('GMT', ''));
|
||||||
|
|
||||||
// Check if there is a cache entry.
|
// Check if there is a cache entry.
|
||||||
|
@ -47,8 +42,9 @@ class ResponseCache {
|
||||||
if (pattern.allMatches(req.uri.path).isNotEmpty &&
|
if (pattern.allMatches(req.uri.path).isNotEmpty &&
|
||||||
_cache.containsKey(req.uri.path)) {
|
_cache.containsKey(req.uri.path)) {
|
||||||
var response = _cache[req.uri.path];
|
var response = _cache[req.uri.path];
|
||||||
|
//print('${response.timestamp} vs ${modifiedSince}');
|
||||||
|
|
||||||
if (response.timestamp.compareTo(modifiedSince) <= 0) {
|
if (response.timestamp.compareTo(modifiedSince) < 0) {
|
||||||
res.statusCode = 304;
|
res.statusCode = 304;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -61,8 +57,7 @@ class ResponseCache {
|
||||||
|
|
||||||
/// Serves content from the cache, if applicable.
|
/// Serves content from the cache, if applicable.
|
||||||
Future<bool> handleRequest(RequestContext req, ResponseContext res) async {
|
Future<bool> handleRequest(RequestContext req, ResponseContext res) async {
|
||||||
if (!await ifModifiedSince(req, res))
|
if (!await ifModifiedSince(req, res)) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check if there is a cache entry.
|
// Check if there is a cache entry.
|
||||||
for (var pattern in patterns) {
|
for (var pattern in patterns) {
|
||||||
|
@ -70,9 +65,13 @@ class ResponseCache {
|
||||||
var now = new DateTime.now().toUtc();
|
var now = new DateTime.now().toUtc();
|
||||||
|
|
||||||
if (_cache.containsKey(req.uri.path)) {
|
if (_cache.containsKey(req.uri.path)) {
|
||||||
// If the cache timeout has been met, don't send the cached response.
|
|
||||||
var response = _cache[req.uri.path];
|
var response = _cache[req.uri.path];
|
||||||
|
|
||||||
|
if (timeout != null) {
|
||||||
|
// If the cache timeout has been met, don't send the cached response.
|
||||||
if (now.difference(response.timestamp) >= timeout) return true;
|
if (now.difference(response.timestamp) >= timeout) return true;
|
||||||
|
}
|
||||||
|
|
||||||
_setCachedHeaders(response.timestamp, req, res);
|
_setCachedHeaders(response.timestamp, req, res);
|
||||||
res
|
res
|
||||||
..headers.addAll(response.headers)
|
..headers.addAll(response.headers)
|
||||||
|
@ -110,7 +109,8 @@ class ResponseCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the response.
|
// Save the response.
|
||||||
var writeLock = _writeLocks.putIfAbsent(req.uri.path, () => new Pool(1));
|
var writeLock =
|
||||||
|
_writeLocks.putIfAbsent(req.uri.path, () => new Pool(1));
|
||||||
await writeLock.withResource(() {
|
await writeLock.withResource(() {
|
||||||
_cache[req.uri.path] = new _CachedResponse(
|
_cache[req.uri.path] = new _CachedResponse(
|
||||||
new Map.from(res.headers), res.buffer.toBytes(), now);
|
new Map.from(res.headers), res.buffer.toBytes(), now);
|
||||||
|
@ -129,11 +129,11 @@ class ResponseCache {
|
||||||
|
|
||||||
res.headers
|
res.headers
|
||||||
..['cache-control'] = '$privacy, max-age=${timeout?.inSeconds ?? 0}'
|
..['cache-control'] = '$privacy, max-age=${timeout?.inSeconds ?? 0}'
|
||||||
..['last-modified'] = _formatDateForHttp(modified);
|
..['last-modified'] = formatDateForHttp(modified);
|
||||||
|
|
||||||
if (timeout != null) {
|
if (timeout != null) {
|
||||||
var expiry = new DateTime.now().add(timeout);
|
var expiry = new DateTime.now().add(timeout);
|
||||||
res.headers['expires'] = _formatDateForHttp(expiry);
|
res.headers['expires'] = formatDateForHttp(expiry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
lib/src/util.dart
Normal file
6
lib/src/util.dart
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
final DateFormat fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss');
|
||||||
|
|
||||||
|
/// Formats a date (converted to UTC), ex: `Sun, 03 May 2015 23:02:37 GMT`.
|
||||||
|
String formatDateForHttp(DateTime dt) => fmt.format(dt.toUtc()) + ' GMT';
|
101
test/cache_test.dart
Normal file
101
test/cache_test.dart
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:angel_cache/src/util.dart';
|
||||||
|
import 'package:angel_cache/angel_cache.dart';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_test/angel_test.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:glob/glob.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
group('no timeout', () {
|
||||||
|
TestClient client;
|
||||||
|
DateTime lastModified;
|
||||||
|
http.Response response1, response2;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
var app = new Angel();
|
||||||
|
var cache = new ResponseCache()
|
||||||
|
..patterns.addAll([
|
||||||
|
new Glob('/*.txt'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
app.use(cache.handleRequest);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/date.txt',
|
||||||
|
(ResponseContext res) =>
|
||||||
|
res.write(new DateTime.now().toIso8601String()));
|
||||||
|
|
||||||
|
app.addRoute('PURGE', '*', (RequestContext req) {
|
||||||
|
cache.purge(req.uri.path);
|
||||||
|
print('Purged ${req.uri.path}');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.responseFinalizers.add(cache.responseFinalizer);
|
||||||
|
|
||||||
|
var oldHandler = app.errorHandler;
|
||||||
|
app.errorHandler = (e, req, res) {
|
||||||
|
if (e.error == null) return oldHandler(e, req, res);
|
||||||
|
return Zone.current.handleUncaughtError(e.error, e.stackTrace);
|
||||||
|
};
|
||||||
|
|
||||||
|
client = await connectTo(app);
|
||||||
|
response1 = await client.get('/date.txt');
|
||||||
|
response2 = await client.get('/date.txt');
|
||||||
|
lastModified = new DateTime.now().toUtc();
|
||||||
|
print('Response 1 status: ${response1.statusCode}');
|
||||||
|
print('Response 2 status: ${response2.statusCode}');
|
||||||
|
print('Response 1 body: ${response1.body}');
|
||||||
|
print('Response 2 body: ${response2.body}');
|
||||||
|
print('Response 1 headers: ${response1.headers}');
|
||||||
|
print('Response 2 headers: ${response2.headers}');
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() => client.close());
|
||||||
|
|
||||||
|
test('saves content', () async {
|
||||||
|
expect(response1.body, response2.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saves headers', () async {
|
||||||
|
response1.headers.forEach((k, v) {
|
||||||
|
expect(response2.headers, containsPair(k, v));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('first response is normal', () {
|
||||||
|
expect(response1.statusCode, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sends last-modified', () {
|
||||||
|
expect(response2.headers.keys, contains('last-modified'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalidate', () async {
|
||||||
|
await client.sendUnstreamed(
|
||||||
|
'PURGE', '/date.txt', {'x-http-method-override': 'PURGE'});
|
||||||
|
var response = await client.get('/date.txt');
|
||||||
|
print('Response after invalidation: ${response.body}');
|
||||||
|
expect(response.body, isNot(response1.body));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sends 304 on if-modified-since', () async {
|
||||||
|
var response = await client.get('/date.txt',
|
||||||
|
headers: {'if-modified-since': formatDateForHttp(lastModified)});
|
||||||
|
expect(response.statusCode, 304);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('last-modified in the past', () async {
|
||||||
|
var response = await client.get('/date.txt', headers: {
|
||||||
|
'if-modified-since':
|
||||||
|
formatDateForHttp(lastModified.subtract(const Duration(days: 10)))
|
||||||
|
});
|
||||||
|
print('Response: ${response.body}');
|
||||||
|
expect(response.statusCode, 200);
|
||||||
|
expect(response.body, isNot(response1.body));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('with timeout', () {});
|
||||||
|
}
|
Loading…
Reference in a new issue