Skip to content
This repository has been archived by the owner on Apr 29, 2021. It is now read-only.

Commit

Permalink
Finish implementation and simple test (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
matanlurey authored Dec 13, 2016
1 parent ef2a542 commit eada46e
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.2.4-alpha

- Added `ReplaySeltzerHttp` and `SeltzerHttpRecorder` for testing

## 0.2.3-alpha

- Added `SeltzerSocketClosedEvent` with information why close occurred
Expand Down
12 changes: 12 additions & 0 deletions lib/platform/testing.dart
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
import 'package:seltzer/src/context.dart';
import 'package:seltzer/src/interface/http.dart';

export 'package:seltzer/src/testing/record.dart' show SeltzerHttpRecorder;
export 'package:seltzer/src/testing/replay.dart' show ReplaySeltzerHttp;

/// Initializes `package:seltzer/seltzer.dart` to use [implementation].
///
/// This is appropriate for test implementations that want to use an existing
/// implementation, such as a [ReplaySeltzerHttp].
void useSeltzerForTesting(SeltzerHttp implementation) {
setHttpPlatform(implementation);
}
5 changes: 2 additions & 3 deletions lib/platform/vm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ class VmSeltzerHttp extends SeltzerHttp {
[Object data]) {
return new HttpClient()
.openUrl(request.method, Uri.parse(request.url))
.asStream()
.asyncMap((r) async {
.then((r) async {
request.headers.forEach(r.headers.add);
final response = await r.close();
final payload = await response.first;
Expand All @@ -45,7 +44,7 @@ class VmSeltzerHttp extends SeltzerHttp {
} else {
return new SeltzerHttpResponse.fromBytes(payload, headers: headers);
}
});
}).asStream();
}
}

Expand Down
1 change: 1 addition & 0 deletions lib/seltzer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export 'package:seltzer/src/context.dart'
export 'package:seltzer/src/interface.dart'
show
SeltzerHttp,
SeltzerHttpHandler,
SeltzerHttpRequest,
SeltzerHttpResponse,
SeltzerWebSocket,
Expand Down
24 changes: 23 additions & 1 deletion lib/src/interface/http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import 'http_response.dart';
///
/// May be implemented to support a simplified model of handling requests.
abstract class SeltzerHttpHandler {
const SeltzerHttpHandler();

/// Executes an HTTP [request].
///
/// If [payload] is specified:
Expand All @@ -16,12 +18,15 @@ abstract class SeltzerHttpHandler {
SeltzerHttpRequest request, [
Object payload,
]);

/// Returns the handler as [SeltzerHttp] instance.
SeltzerHttp asHttpClient() => new _AsHttpClient(this);
}

/// Elegant and rich cross-platform HTTP service.
///
/// See `platform/browser.dart` and `platform/vm.dart` for implementations.
abstract class SeltzerHttp implements SeltzerHttpHandler {
abstract class SeltzerHttp extends SeltzerHttpHandler {
const SeltzerHttp();

/// Create a request to DELETE from [url].
Expand All @@ -47,3 +52,20 @@ abstract class SeltzerHttp implements SeltzerHttpHandler {
);
}
}

class _AsHttpClient extends SeltzerHttp {
final SeltzerHttpHandler _handler;

const _AsHttpClient(this._handler);

@override
SeltzerHttp asHttpClient() => this;

@override
Stream<SeltzerHttpResponse> handle(
SeltzerHttpRequest request, [
Object payload,
]) {
return _handler.handle(request, payload);
}
}
30 changes: 29 additions & 1 deletion lib/src/interface/http_request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ import 'package:meta/meta.dart';
import 'package:quiver/core.dart';

import 'http.dart';
import 'package:seltzer/src/interface/http_response.dart';

class _NullSeltzerHttpHandler extends SeltzerHttpHandler {
const _NullSeltzerHttpHandler();

@override
Stream<SeltzerHttpResponse> handle(SeltzerHttpRequest request, [_]) {
throw new UnsupportedError(
'Cannot send requests - this was created as a standalone object',
);
}
}

/// An HTTP request object.
///
Expand All @@ -18,6 +30,19 @@ import 'http.dart';
/// the server. In that case, [Stream.listen] may be preferred:
/// get('some/url.json').send().listen((value) => print('Got: $value'));
abstract class SeltzerHttpRequest {
/// Creates a new standalone request that _cannot be sent_.
factory SeltzerHttpRequest(
String method,
String url, {
Map<String, String> headers: const {},
}) =>
new SeltzerHttpRequest.fromHandler(
const _NullSeltzerHttpHandler(),
headers: headers,
method: method,
url: url,
);

/// Create a default implementation from a [handler].
factory SeltzerHttpRequest.fromHandler(
SeltzerHttpHandler handler, {
Expand All @@ -36,7 +61,7 @@ abstract class SeltzerHttpRequest {
String get url;

/// Makes the HTTP request, and returns a [Stream] of results.
Stream<dynamic> send([Object payload]);
Stream<SeltzerHttpResponse> send([Object payload]);
}

/// A partial implementation of [SeltzerHttpRequest].
Expand Down Expand Up @@ -77,6 +102,9 @@ abstract class SeltzerHttpRequestBase extends SeltzerHtpRequestMixin {
@required this.url,
})
: this.headers = headers ?? <String, String>{};

@override
String toString() => '$method $url {headers: ${headers.length}}';
}

/// A reusable [Equality] implementation for [SeltzerHttpRequest].
Expand Down
46 changes: 46 additions & 0 deletions lib/src/testing/record.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'dart:async';

import 'package:reply/reply.dart';
import 'package:seltzer/seltzer.dart';
import 'package:seltzer/src/interface/http_request.dart';

/// An interceptor/delegate [SeltzerHttp] that records request/response pairs.
class SeltzerHttpRecorder extends SeltzerHttpHandler {
final SeltzerHttpHandler _delegate;
final Recorder<SeltzerHttpRequest, SeltzerHttpResponse> _recorder;

factory SeltzerHttpRecorder(SeltzerHttpHandler delegate) {
return new SeltzerHttpRecorder._(
delegate,
new Recorder<SeltzerHttpRequest, SeltzerHttpResponse>(
requestEquality: const SeltzerHttpRequestEquality(),
),
);
}

SeltzerHttpRecorder._(this._delegate, this._recorder);

@override
Stream<SeltzerHttpResponse> handle(
SeltzerHttpRequest request, [
Object payload,
]) {
SeltzerHttpResponse last;
final transformer = new StreamTransformer.fromHandlers(
handleData: (event, sink) {
last = event;
sink.add(event);
},
handleDone: (sink) {
_recorder.given(request).reply(last).once();
sink.close();
},
);
return _delegate.handle(request, payload).transform(transformer);
}

/// Returns all recorded request/response pairs.
Recording<SeltzerHttpRequest, SeltzerHttpResponse> toRecording() {
return _recorder.toRecording();
}
}
37 changes: 37 additions & 0 deletions lib/src/testing/replay.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'dart:async';

import 'package:reply/reply.dart';
import 'package:seltzer/seltzer.dart';
import 'package:seltzer/src/interface/http_request.dart';

/// An implementation of [SeltzerHttp] that plays back a recording.
class ReplaySeltzerHttp extends SeltzerHttp {
final Recording<SeltzerHttpRequest, SeltzerHttpResponse> _recording;

/// Create a new [ReplaySeltzerHttp] from a previous [recording].
factory ReplaySeltzerHttp(
Recording<SeltzerHttpRequest, SeltzerHttpResponse> recording,
) = ReplaySeltzerHttp._;

/// Creates a new [ReplySeltzerHttp] from [pairs] of request/responses.
factory ReplaySeltzerHttp.fromMap(
Map<SeltzerHttpRequest, SeltzerHttpResponse> pairs,
) {
final recorder = new Recorder<SeltzerHttpRequest, SeltzerHttpResponse>(
requestEquality: const SeltzerHttpRequestEquality(),
);
pairs.forEach((request, response) {
recorder.given(request).reply(response).once();
});
return new ReplaySeltzerHttp._(recorder.toRecording());
}

ReplaySeltzerHttp._(this._recording);

@override
Stream<SeltzerHttpResponse> handle(SeltzerHttpRequest request, [_]) {
return new Stream<SeltzerHttpResponse>.fromIterable([
_recording.reply(request),
]);
}
}
6 changes: 5 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
name: seltzer
version: 0.2.3-alpha
version: 0.2.4-alpha
description: An elegant and rich cross-platform HTTP library for Dart.
authors:
- Matan Lurey <[email protected]>
- Kendal Harland <[email protected]>
homepage: https://github.com/matanlurey/seltzer

environment:
sdk: ">=1.8.0 <2.0.0"

dependencies:
meta: "^1.0.4"
reply: "0.1.2-dev"
quiver: "^0.23.0"

dev_dependencies:
dart_style: ">=0.2.10"
test: ">=0.12.15"
45 changes: 45 additions & 0 deletions test/runtime/record_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@TestOn('vm')
import 'dart:convert';

import 'package:seltzer/platform/testing.dart';
import 'package:seltzer/platform/vm.dart';
import 'package:test/test.dart';

const _echoUrl = 'http://localhost:9090';

main() {
SeltzerHttp http;
SeltzerHttpRecorder recorder;

setUp(() {
recorder = new SeltzerHttpRecorder(const VmSeltzerHttp());
http = recorder.asHttpClient();
});

runPingTest() async {
final response = await http.post('$_echoUrl/ping').send().last;
expect(JSON.decode(response.readAsString()), {
'data': '',
'headers': {},
'method': 'POST',
'url': '/ping',
});
}

test('should record a request/response', () async {
await runPingTest();
final recording = recorder.toRecording();

expect(
recording.hasRecord(new SeltzerHttpRequest(
'POST',
'$_echoUrl/ping',
)),
isTrue,
);

// Now lets try replaying without a real HTTP connection.
http = new ReplaySeltzerHttp(recording);
await runPingTest();
});
}
85 changes: 85 additions & 0 deletions test/runtime/replay_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'dart:convert';

import 'package:seltzer/seltzer.dart';
import 'package:seltzer/platform/testing.dart';

import '../common/http.dart';

main() {
useSeltzerForTesting(
new ReplaySeltzerHttp.fromMap({
// "should make a valid DELETE request"
new SeltzerHttpRequest(
'DELETE',
'http://localhost:9090/das/fridge/lacroix',
): new SeltzerHttpResponse.fromString(JSON.encode({
'headers': {},
'method': 'DELETE',
'url': '/das/fridge/lacroix',
'data': '',
})),

// "should make a valid GET request"
new SeltzerHttpRequest(
'GET',
'http://localhost:9090/flags.json',
): new SeltzerHttpResponse.fromString(JSON.encode({
'headers': {},
'method': 'GET',
'url': '/flags.json',
'data': '',
})),

// "should make a valid PATCH request"
new SeltzerHttpRequest(
'PATCH',
'http://localhost:9090/pants/up',
): new SeltzerHttpResponse.fromString(JSON.encode({
'headers': {},
'method': 'PATCH',
'url': '/pants/up',
'data': '',
})),

// "should make a valid POST request"
new SeltzerHttpRequest(
'POST',
'http://localhost:9090/users/clear',
): new SeltzerHttpResponse.fromString(JSON.encode({
'headers': {},
'method': 'POST',
'url': '/users/clear',
'data': '',
})),

// "should make a valid PUT request"
new SeltzerHttpRequest(
'PUT',
'http://localhost:9090/pants/on',
): new SeltzerHttpResponse.fromString(JSON.encode({
'headers': {},
'method': 'PUT',
'url': '/pants/on',
'data': '',
})),

// "should send an HTTP header"
new SeltzerHttpRequest(
'GET',
'http://localhost:9090',
headers: {
'Authorization': 'abc123',
},
): new SeltzerHttpResponse.fromString(JSON.encode({
'headers': {
'Authorization': 'abc123',
},
'method': 'GET',
'url': '/',
'data': '',
}))
}),
);

runHttpTests();
}

0 comments on commit eada46e

Please sign in to comment.