Complete unit tests

This commit is contained in:
Tobe O 2019-02-07 11:46:47 -05:00
parent 405d4d4b27
commit ae80b03c1d
5 changed files with 247 additions and 102 deletions

View file

@ -0,0 +1 @@

View file

@ -1,82 +1,100 @@
/// Angel CORS middleware.
library angel_cors;
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import 'src/cors_options.dart';
export 'src/cors_options.dart';
/// Determines if a request origin is CORS-able.
typedef bool CorsFilter(String origin);
typedef bool _CorsFilter(String origin);
bool _isOriginAllowed(String origin, allowedOrigin) {
bool _isOriginAllowed(String origin, [allowedOrigin]) {
allowedOrigin ??= [];
if (allowedOrigin is List) {
if (allowedOrigin is Iterable) {
return allowedOrigin.any((x) => _isOriginAllowed(origin, x));
} else if (allowedOrigin is String) {
return origin == allowedOrigin;
} else if (allowedOrigin is RegExp) {
return origin != null && allowedOrigin.hasMatch(origin);
} else if (origin != null && allowedOrigin is CorsFilter) {
} else if (origin != null && allowedOrigin is _CorsFilter) {
return allowedOrigin(origin);
} else {
return allowedOrigin != false;
}
}
/// On-the-fly configures the [cors] handler. Use this when the context of the surrounding request
/// is necessary to decide how to handle an incoming request.
Future<bool> Function(RequestContext, ResponseContext) dynamicCors(
FutureOr<CorsOptions> Function(RequestContext, ResponseContext) f) {
return (req, res) async {
var opts = await f(req, res);
var handler = cors(opts);
return await handler(req, res);
};
}
/// Applies the given [CorsOptions].
RequestMiddleware cors([CorsOptions options]) {
final opts = options ?? new CorsOptions();
Future<bool> Function(RequestContext, ResponseContext) cors(
[CorsOptions options]) {
options ??= CorsOptions();
return (RequestContext req, ResponseContext res) async {
// Access-Control-Allow-Credentials
if (opts.credentials == true) {
res.headers['Access-Control-Allow-Credentials'] = 'true';
return (req, res) async {
// access-control-allow-credentials
if (options.credentials == true) {
res.headers['access-control-allow-credentials'] = 'true';
}
// Access-Control-Allow-Headers
if (req.method == 'OPTIONS' && opts.allowedHeaders.isNotEmpty) {
res.headers['Access-Control-Allow-Headers'] =
opts.allowedHeaders.join(',');
} else if (req.headers['Access-Control-Request-Headers'] != null) {
res.headers['Access-Control-Allow-Headers'] =
req.headers.value('Access-Control-Request-Headers');
// access-control-allow-headers
if (req.method == 'OPTIONS' && options.allowedHeaders.isNotEmpty) {
res.headers['access-control-allow-headers'] =
options.allowedHeaders.join(',');
} else if (req.headers['access-control-request-headers'] != null) {
res.headers['access-control-allow-headers'] =
req.headers.value('access-control-request-headers');
}
// Access-Control-Expose-Headers
if (opts.exposedHeaders.isNotEmpty) {
res.headers['Access-Control-Expose-Headers'] =
opts.exposedHeaders.join(',');
// access-control-expose-headers
if (options.exposedHeaders.isNotEmpty) {
res.headers['access-control-expose-headers'] =
options.exposedHeaders.join(',');
}
// Access-Control-Allow-Methods
if (req.method == 'OPTIONS' && opts.methods.isNotEmpty) {
res.headers['Access-Control-Allow-Methods'] = opts.methods.join(',');
// access-control-allow-methods
if (req.method == 'OPTIONS' && options.methods.isNotEmpty) {
res.headers['access-control-allow-methods'] = options.methods.join(',');
}
// Access-Control-Max-Age
if (req.method == 'OPTIONS' && opts.maxAge != null) {
res.headers['Access-Control-Max-Age'] = opts.maxAge.toString();
// access-control-max-age
if (req.method == 'OPTIONS' && options.maxAge != null) {
res.headers['access-control-max-age'] = options.maxAge.toString();
}
// Access-Control-Allow-Origin
if (opts.origin == false || opts.origin == '*') {
res.headers['Access-Control-Allow-Origin'] = '*';
} else if (opts.origin is String) {
// access-control-allow-origin
if (options.origin == false || options.origin == '*') {
res.headers['access-control-allow-origin'] = '*';
} else if (options.origin is String) {
res
..headers['Access-Control-Allow-Origin'] = opts.origin
..headers['Vary'] = 'Origin';
..headers['access-control-allow-origin'] = options.origin as String
..headers['vary'] = 'origin';
} else {
bool isAllowed =
_isOriginAllowed(req.headers.value('Origin'), opts.origin);
_isOriginAllowed(req.headers.value('origin'), options.origin);
res.headers['Access-Control-Allow-Origin'] =
isAllowed ? req.headers.value('Origin') : false.toString();
res.headers['access-control-allow-origin'] =
isAllowed ? req.headers.value('origin') : false.toString();
if (isAllowed) {
res.headers['Vary'] = 'Origin';
res.headers['vary'] = 'origin';
}
}
return req.method != 'OPTIONS' || opts.preflightContinue;
if (req.method != 'OPTIONS') return true;
res.statusCode = options.successStatus ?? 204;
res.contentLength = 0;
await res.close();
return options.preflightContinue;
};
}

View file

@ -1,5 +1,5 @@
/// CORS configuration options.
///
///
/// The default configuration is the equivalent of:
///
///```json
@ -14,7 +14,7 @@ class CorsOptions {
final List<String> allowedHeaders = [];
/// Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header, otherwise it is omitted.
bool credentials;
final bool credentials;
/// Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range') or an array (ex: `['Content-Range', 'X-Content-Range']`). If not specified, no custom headers are exposed.
final List<String> exposedHeaders = [];
@ -22,7 +22,10 @@ class CorsOptions {
/// Configures the **Access-Control-Max-Age** CORS header. Set to an integer to pass the header, otherwise it is omitted.
///
/// Default: `null`
int maxAge = null;
final int maxAge;
/// The status code to be sent on successful `OPTIONS` requests, if [preflightContinue] is `false`.
final int successStatus;
/// Configures the **Access-Control-Allow-Methods** CORS header. Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: `['GET', 'PUT', 'POST']`).
///
@ -35,21 +38,21 @@ class CorsOptions {
/// - `String` - set `origin` to a specific origin. For example if you set it to `"http://example.com"` only requests from "http://example.com" will be allowed.
/// - `RegExp` - set `origin` to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern `/example\.com$/` will reflect any request that is coming from an origin ending with "example.com".
/// - `Array` - set `origin` to an array of valid origins. Each origin can be a `String` or a `RegExp`. For example `["http://example1.com", /\.example2\.com$/]` will accept any request from "http://example1.com" or from a subdomain of "example2.com".
/// - `Function` - set `origin` to a function implementing some custom logic. The function takes the request origin as the first parameter and a callback (which expects the signature `err [object], allow [bool]`) as the second.
/// - `bool Function(String)` - set `origin` to a function implementing some custom logic. The function takes the request origin as the first parameter and returns a [bool].
///
/// Default: `'*'`
var origin;
final origin;
/// Pass the CORS preflight response to the next handler.
/// If `false`, then the [cors] handler will terminate the response after performing its logic.
///
/// Default: `false`
bool preflightContinue;
final bool preflightContinue;
CorsOptions(
{List<String> allowedHeaders: const [],
{Iterable<String> allowedHeaders = const [],
this.credentials,
this.maxAge,
List<String> methods: const [
Iterable<String> methods = const [
'GET',
'HEAD',
'PUT',
@ -57,9 +60,10 @@ class CorsOptions {
'POST',
'DELETE'
],
this.origin: '*',
this.preflightContinue: false,
List<String> exposedHeaders: const []}) {
this.origin = '*',
this.successStatus = 204,
this.preflightContinue = false,
Iterable<String> exposedHeaders = const []}) {
if (allowedHeaders != null) this.allowedHeaders.addAll(allowedHeaders);
if (methods != null) this.methods.addAll(methods);

View file

@ -1,52 +0,0 @@
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_cors/angel_cors.dart';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';
main() {
Angel app;
http.Client client;
HttpServer server;
String url;
setUp(() async {
app = new Angel()
..before.add(cors(new CorsOptions()))
..post('/', (req, res) async {
res.write('hello world');
return false;
})
..all('*', () {
throw new AngelHttpException.notFound();
});
server = await app.startServer();
url = 'http://${server.address.address}:${server.port}';
client = new http.Client();
});
tearDown(() async {
await server.close(force: true);
app = null;
client = null;
url = null;
});
test('POST works', () async {
final response = await client.post(url);
expect(response.statusCode, equals(200));
print('Response: ${response.body}');
print('Headers: ${response.headers}');
expect(response.headers['access-control-allow-origin'], equals('*'));
});
test('mirror headers', () async {
final response = await client
.post(url, headers: {'access-control-request-headers': 'foo'});
expect(response.statusCode, equals(200));
print('Response: ${response.body}');
print('Headers: ${response.headers}');
expect(response.headers['access-control-allow-headers'], equals('foo'));
});
}

174
test/cors_test.dart Normal file
View file

@ -0,0 +1,174 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_cors/angel_cors.dart';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';
main() {
Angel app;
AngelHttp server;
http.Client client;
setUp(() async {
app = Angel()
..options('/credentials', cors(CorsOptions(credentials: true)))
..options('/credentials_d',
dynamicCors((req, res) => CorsOptions(credentials: true)))
..options(
'/headers', cors(CorsOptions(exposedHeaders: ['x-foo', 'x-bar'])))
..options('/max_age', cors(CorsOptions(maxAge: 250)))
..options('/methods', cors(CorsOptions(methods: ['GET', 'POST'])))
..get(
'/originl',
chain([
cors(CorsOptions(
origin: ['foo.bar', 'baz.quux'],
)),
(req, res) => req.headers['origin']
]))
..get(
'/origins',
chain([
cors(CorsOptions(
origin: 'foo.bar',
)),
(req, res) => req.headers['origin']
]))
..get(
'/originr',
chain([
cors(CorsOptions(
origin: RegExp(r'^foo\.[^x]+$'),
)),
(req, res) => req.headers['origin']
]))
..get(
'/originp',
chain([
cors(CorsOptions(
origin: (String s) => s.endsWith('.bar'),
)),
(req, res) => req.headers['origin']
]))
..options('/status', cors(CorsOptions(successStatus: 418)))
..fallback(cors(CorsOptions()))
..post('/', (req, res) async {
res.write('hello world');
})
..fallback((req, res) => throw AngelHttpException.notFound());
server = AngelHttp(app);
await server.startServer('127.0.0.1', 0);
client = http.Client();
});
tearDown(() async {
await server.close();
app = null;
client = null;
});
test('status 204 by default', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/max_age'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.statusCode, 204);
});
test('content length 0 by default', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/max_age'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.contentLength, 0);
});
test('custom successStatus', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/status'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.statusCode, 418);
});
test('max age', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/max_age'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-max-age'], '250');
});
test('methods', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/methods'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-allow-methods'], 'GET,POST');
});
test('dynamicCors.credentials', () async {
var rq =
http.Request('OPTIONS', server.uri.replace(path: '/credentials_d'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-allow-credentials'], 'true');
});
test('credentials', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/credentials'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-allow-credentials'], 'true');
});
test('exposed headers', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/headers'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-expose-headers'], 'x-foo,x-bar');
});
test('invalid origin', () async {
var response = await client.get(server.uri.replace(path: '/originl'),
headers: {'origin': 'foreign'});
expect(response.headers['access-control-allow-origin'], 'false');
});
test('list origin', () async {
var response = await client.get(server.uri.replace(path: '/originl'),
headers: {'origin': 'foo.bar'});
expect(response.headers['access-control-allow-origin'], 'foo.bar');
expect(response.headers['vary'], 'origin');
response = await client.get(server.uri.replace(path: '/originl'),
headers: {'origin': 'baz.quux'});
expect(response.headers['access-control-allow-origin'], 'baz.quux');
expect(response.headers['vary'], 'origin');
});
test('string origin', () async {
var response = await client.get(server.uri.replace(path: '/origins'),
headers: {'origin': 'foo.bar'});
expect(response.headers['access-control-allow-origin'], 'foo.bar');
expect(response.headers['vary'], 'origin');
});
test('regex origin', () async {
var response = await client.get(server.uri.replace(path: '/originr'),
headers: {'origin': 'foo.bar'});
expect(response.headers['access-control-allow-origin'], 'foo.bar');
expect(response.headers['vary'], 'origin');
});
test('predicate origin', () async {
var response = await client.get(server.uri.replace(path: '/originp'),
headers: {'origin': 'foo.bar'});
expect(response.headers['access-control-allow-origin'], 'foo.bar');
expect(response.headers['vary'], 'origin');
});
test('POST works', () async {
final response = await client.post(server.uri);
expect(response.statusCode, equals(200));
print('Response: ${response.body}');
print('Headers: ${response.headers}');
expect(response.headers['access-control-allow-origin'], equals('*'));
});
test('mirror headers', () async {
final response = await client
.post(server.uri, headers: {'access-control-request-headers': 'foo'});
expect(response.statusCode, equals(200));
print('Response: ${response.body}');
print('Headers: ${response.headers}');
expect(response.headers['access-control-allow-headers'], equals('foo'));
});
}