Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dart_frog): Pipeline does not maintain RequestContext #605

Merged
merged 3 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions examples/counter/test/routes/_middleware_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,22 @@ class _MockRequestContext extends Mock implements RequestContext {}
void main() {
group('middleware', () {
test('provides incremented count', () async {
int? count;
final handler = middleware(
(context) {
count = context.read<int>();
return Response(body: '');
},
);
final handler = middleware((context) => Response());
final request = Request.get(Uri.parse('http://localhost/'));
final context = _MockRequestContext();

when(() => context.request).thenReturn(request);
when(() => context.provide<int>(any())).thenReturn(context);

await handler(context);
expect(count, equals(1));

await handler(context);
expect(count, equals(2));
final create = verify(() => context.provide<int>(captureAny()))
.captured
.single as int Function();

await handler(context);
expect(count, equals(3));
expect(create(), equals(1));
expect(create(), equals(2));
expect(create(), equals(3));
});
});
}
17 changes: 9 additions & 8 deletions examples/kitchen_sink/test/routes/_middleware_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ class _MockRequestContext extends Mock implements RequestContext {}
void main() {
group('middleware', () {
test('provides greeting', () async {
String? greeting;
final handler = middleware(
(context) {
greeting = context.read<String>();
return Response(body: '');
},
);
final handler = middleware((_) => Response());
final request = Request.get(Uri.parse('http://localhost/'));
final context = _MockRequestContext();

when(() => context.request).thenReturn(request);
when(() => context.provide<String>(any())).thenReturn(context);

await handler(context);
expect(greeting, equals('Hello'));

final create = verify(() => context.provide<String>(captureAny()))
.captured
.single as String Function();

expect(create(), equals('Hello'));
});
});
}
16 changes: 8 additions & 8 deletions examples/web_socket_counter/test/routes/_middleware_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ class _MockRequestContext extends Mock implements RequestContext {}
void main() {
group('middleware', () {
test('provides a CounterCubit instance.', () async {
CounterCubit? cubit;
final handler = middleware(
(context) {
cubit = context.read<CounterCubit>();
return Response();
},
);
final handler = middleware((_) => Response());
final request = Request.get(Uri.parse('http://localhost/'));
final context = _MockRequestContext();

when(() => context.request).thenReturn(request);
when(() => context.provide<CounterCubit>(any())).thenReturn(context);

await handler(context);
expect(cubit, isNotNull);

final create = verify(() => context.provide<CounterCubit>(captureAny()))
.captured
.single as CounterCubit Function();
expect(create(), isA<CounterCubit>());
});
});
}
44 changes: 14 additions & 30 deletions packages/dart_frog/lib/src/pipeline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,28 @@ part of '_internal.dart';
/// {@endtemplate}
class Pipeline {
/// {@macro pipeline}
const Pipeline() : this._(const shelf.Pipeline());

const Pipeline._(this._pipeline);

final shelf.Pipeline _pipeline;
const Pipeline();

/// Returns a new [Pipeline] with [middleware] added to the existing set of
/// [Middleware].
///
/// [middleware] will be the last [Middleware] to process a request and
/// the first to process a response.
Pipeline addMiddleware(Middleware middleware) {
return Pipeline._(
_pipeline.addMiddleware((innerHandler) {
return (request) async {
final response = await middleware(
(context) async {
final response = await innerHandler(context.request._request);
return Response._(response);
},
)(RequestContext._(request));
return response._response;
};
}),
);
}
Pipeline addMiddleware(Middleware middleware) =>
_Pipeline(middleware, addHandler);

/// Returns a new [Handler] with [handler] as the final processor of a
/// [Request] if all of the middleware in the pipeline have passed the request
/// through.
Handler addHandler(Handler handler) {
return (context) async {
final response = await _pipeline.addHandler((request) async {
final context = RequestContext._(request);
final response = await handler(context);
return response._response;
})(context.request._request);
return Response._(response);
};
}
Handler addHandler(Handler handler) => handler;
}

class _Pipeline extends Pipeline {
_Pipeline(this._middleware, this._parent);

final Middleware _middleware;
final Middleware _parent;

@override
Handler addHandler(Handler handler) => _parent(_middleware(handler));
}
38 changes: 30 additions & 8 deletions packages/dart_frog/test/src/middleware_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

Expand Down Expand Up @@ -31,16 +32,14 @@ void main() {
final handler =
const Pipeline().addMiddleware(middleware).addHandler(onRequest);

final request = Request.get(Uri.parse('http://localhost/'));
final context = _MockRequestContext();
when(() => context.request).thenReturn(request);
final response = await handler(context);
final server = await serve(handler, 'localhost', 3020);
final client = http.Client();
final response = await client.get(Uri.parse('http://localhost:3020/'));

await expectLater(response.statusCode, equals(HttpStatus.ok));
await expectLater(
await response.body(),
equals('$stringValue $intValue'),
);
await expectLater(response.body, equals('$stringValue $intValue'));

await server.close();
});

test('middleware can be used to read the request body', () async {
Expand Down Expand Up @@ -119,4 +118,27 @@ void main() {
expect(response.statusCode, equals(HttpStatus.ok));
expect(response.body(), completion(equals(body)));
});

test('chaining middleware retains request context', () async {
const value = 'test-value';
Middleware noop() => (handler) => (context) => handler(context);

Future<Response> onRequest(RequestContext context) async {
final value = context.read<String>();
return Response(body: value);
}

final handler =
const Pipeline().addMiddleware(noop()).addHandler(onRequest);
final request = Request.get(Uri.parse('http://localhost/'));
final context = _MockRequestContext();

when(() => context.read<String>()).thenReturn(value);
when(() => context.request).thenReturn(request);

final response = await handler(context);

expect(response.statusCode, equals(HttpStatus.ok));
expect(response.body(), completion(equals(value)));
});
}
44 changes: 26 additions & 18 deletions packages/dart_frog/test/src/provider_test.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';
import 'package:mocktail/mocktail.dart';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';

class _MockRequestContext extends Mock implements RequestContext {}

void main() {
test('values can be provided and read via middleware', () async {
const value = '__test_value__';
Expand All @@ -21,13 +19,14 @@ void main() {
final handler =
const Pipeline().addMiddleware(middleware).addHandler(onRequest);

final request = Request.get(Uri.parse('http://localhost/'));
final context = _MockRequestContext();
when(() => context.request).thenReturn(request);
final response = await handler(context);
final server = await serve(handler, 'localhost', 3010);
final client = http.Client();
final response = await client.get(Uri.parse('http://localhost:3010/'));

await expectLater(response.statusCode, equals(HttpStatus.ok));
await expectLater(await response.body(), equals(value));
await expectLater(response.body, equals(value));

await server.close();
});

test('descendant providers can access provided values', () async {
Expand All @@ -46,29 +45,38 @@ void main() {
final handler =
const Pipeline().addMiddleware(middleware).addHandler(onRequest);

final request = Request.get(Uri.parse('http://localhost/'));
final context = _MockRequestContext();
when(() => context.request).thenReturn(request);
final response = await handler(context);
final server = await serve(handler, 'localhost', 3011);
final client = http.Client();
final response = await client.get(Uri.parse('http://localhost:3011/'));

await expectLater(response.statusCode, equals(HttpStatus.ok));
await expectLater(await response.body(), equals(url));
await expectLater(response.body, equals(url));

await server.close();
});

test('A StateError is thrown when reading an un-provided value', () async {
Object? exception;
Response onRequest(RequestContext context) {
context.read<Uri>();
try {
context.read<Uri>();
} catch (e) {
exception = e;
}
return Response();
}

final handler = const Pipeline()
.addMiddleware((handler) => handler)
.addHandler(onRequest);

final request = Request.get(Uri.parse('http://localhost/'));
final context = _MockRequestContext();
when(() => context.request).thenReturn(request);
final server = await serve(handler, 'localhost', 3012);
final client = http.Client();
final response = await client.get(Uri.parse('http://localhost:3012/'));

await expectLater(response.statusCode, equals(HttpStatus.ok));
expect(exception, isA<StateError>());

await expectLater(() => handler(context), throwsStateError);
await server.close();
});
}