Complete unit tests
This commit is contained in:
parent
405d4d4b27
commit
ae80b03c1d
5 changed files with 247 additions and 102 deletions
|
@ -0,0 +1 @@
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
174
test/cors_test.dart
Normal 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'));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue