Skip to content

Commit

Permalink
Added testing utilities used for recording and replaying Reddit API i…
Browse files Browse the repository at this point in the history
…nteractions (#7)

* Created TestAuthenticator, a wrapper class for an Authenticator that is
used to record and replay Reddit API interactions.

Added a basic test to show an example of how to use a record for
testing.

* Fixed analyzer errors.

* Moved test package to dependencies.

* Updated .travis.yml
  • Loading branch information
bkonyi committed Aug 4, 2017
1 parent 05b245e commit dff27f2
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 4 deletions.
4 changes: 1 addition & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,4 @@ dart:
dart_task:
- test: -p vm
- dartfmt

script:
- dartanalyzer --strong --fatal-warnings --fatal-lints .
- dartanalyzer --strong --fatal-warnings --fatal-lints
7 changes: 7 additions & 0 deletions lib/src/reddit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ class Reddit {
}
}

Reddit.fromAuthenticator(Authenticator auth) {
if (auth == null) {
throw new DRAWAuthenticationError('auth cannot be null.');
}
_initializationCallback(auth);
}

void _initializationCallback(Authenticator auth) {
_auth = auth;
_initializedCompleter.complete(true);
Expand Down
3 changes: 2 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ authors:
dependencies:
http: 'any'
oauth2: '>= 1.1.0'
test: '^0.12.5'

dev_dependencies:
test: '^0.12.5'
reply: '^0.1.2-dev'
28 changes: 28 additions & 0 deletions test/api_v1_me_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) 2017, the Dart Reddit API Wrapper project authors.
// Please see the AUTHORS file for details. All rights reserved.
// Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

import 'dart:async';

import 'package:test/test.dart';
import 'package:draw/draw.dart';
import 'package:draw/src/auth.dart';

import 'test_authenticator.dart';

Future main() async {
test('api/v1/me_RawTest', () async {
final testAuth = new TestAuthenticator('test/records/api_v1_me_raw.json');
final reddit = new Reddit.fromAuthenticator(testAuth);
await reddit.initialized;
final response =
await reddit.auth.get(Uri.parse('https://oauth.reddit.com/api/v1/me'));
expect(response['is_employee'], equals(false));
expect(response['name'], equals('DRAWApiOfficial'));
expect(response['created'], equals(1501830779.0));
expect(response['features'], isNot(null));
final features = response['features'];
expect(features['do_not_track'], equals(true));
});
}
1 change: 1 addition & 0 deletions test/records/api_v1_me_raw.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"always":true,"request":["https://oauth.reddit.com/api/v1/me"],"response":{"is_employee":false,"features":{"do_not_track":true,"show_amp_link":true,"live_happening_now":true,"adserver_reporting":true,"give_hsts_grants":true,"legacy_search_pref":true,"listing_service_rampup":true,"mobile_web_targeting":true,"default_srs_holdout":{"owner":"relevance","variant":"control_2","experiment_id":171},"adzerk_do_not_track":true,"users_listing":true,"show_user_sr_name":true,"whitelisted_pms":true,"sticky_comments":true,"personalization_prefs":true,"upgrade_cookies":true,"interest_targeting":true,"new_report_flow":true,"block_user_by_report":true,"post_embed":true,"ads_auto_refund":true,"orangereds_as_emails":true,"mweb_xpromo_modal_listing_click_daily_dismissible_ios":true,"mweb_xpromo_ad_feed_android":{"owner":"channels","variant":"control_2","experiment_id":195},"expando_events":true,"eu_cookie_policy":true,"utm_comment_links":true,"force_https":true,"mobile_native_banner":true,"post_to_profile_beta":true,"reddituploads_redirect":true,"outbound_clicktracking":true,"new_loggedin_cache_policy":true,"inbox_push":true,"https_redirect":true,"search_dark_traffic":true,"mweb_xpromo_interstitial_comments_ios":true,"pause_ads":true,"programmatic_ads":true,"geopopular":true,"show_recommended_link":true,"mweb_xpromo_interstitial_comments_android":true,"ads_auction":true,"screenview_events":true,"new_report_dialog":true,"moat_tracking":true,"subreddit_rules":true,"mobile_settings":true,"adzerk_reporting_2":true,"activity_service_write":true,"ads_auto_extend":true,"geopopular_au_holdout":{"owner":"relevance","variant":"control_2","experiment_id":206},"scroll_events":true,"mweb_xpromo_modal_listing_click_daily_dismissible_android":true,"adblock_test":true,"activity_service_read":true},"pref_no_profanity":true,"is_suspended":false,"pref_geopopular":"","subreddit":null,"is_sponsor":false,"gold_expiration":null,"id":"9b8dzfo","suspension_expiration_utc":null,"verified":false,"new_modmail_exists":true,"over_18":false,"is_gold":false,"is_mod":true,"has_verified_email":false,"has_mod_mail":false,"oauth_client_id":"Db_4C6XcNCNqow","hide_from_robots":false,"link_karma":1,"inbox_count":1,"pref_top_karma_subreddits":null,"has_mail":true,"pref_show_snoovatar":false,"name":"DRAWApiOfficial","created":1501830779.0,"gold_creddits":0,"created_utc":1501801979.0,"in_beta":false,"comment_karma":0,"has_subscribed":false}}]
107 changes: 107 additions & 0 deletions test/test_authenticator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) 2017, the Dart Reddit API Wrapper project authors.
// Please see the AUTHORS file for details. All rights reserved.
// Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:collection/collection.dart';
import 'package:reply/reply.dart';

import 'package:draw/src/auth.dart';
import 'package:draw/src/exceptions.dart';

/// A drop-in replacement for [Authenticator], used for recording and replaying
/// Reddit API interactions, used primarily for testing.
class TestAuthenticator extends Authenticator {
final String _recordingPath;
final Authenticator _recordAuth;
final _recorder = new Recorder<List, dynamic>();
bool get isRecording => (_recordAuth == null);
Recording _recording;

/// Creates a [TestAuthenticator] object which either reads a recording from
/// [recordingPath] or records Reddit API requests and responses if
/// [recordAuth] is provided. If [recordAuth] is provided, it must be a
/// valid Authenticator with valid OAuth2 credentials that is capable of
/// making requests to the Reddit API. Note: when recording Reddit API
/// interactions, [writeRecording] must be called to write all prior records
/// to the file at [recordingPath].
TestAuthenticator(String recordingPath, {Authenticator recordAuth})
: _recordingPath = recordingPath,
_recordAuth = recordAuth,
super(null, null) {
if (isRecording) {
final rawRecording = new File(recordingPath).readAsStringSync();
_recording = new Recording.fromJson(JSON.decode(rawRecording),
toRequest: (q) => q,
toResponse: (r) => r,
requestEquality: const ListEquality());
}
}

@override
Future refresh() async {
if (isRecording) {
throw new DRAWAuthenticationError('cannot refresh a TestAuthenticator.');
}
return _recordAuth.refresh();
}

@override
Future revoke() async {
if (isRecording) {
throw new DRAWAuthenticationError('cannot revoke a TestAuthenticator.');
}
return _recordAuth.revoke();
}

@override
Future<Map> get(Uri path) async {
Map result;
if (isRecording) {
// TODO(bkonyi): grab the response based on query.
return _recording.reply([path.toString()]);
} else {
print(path.toString());
result = await _recordAuth.get(path);
// TODO(bkonyi): do we always want to reply?
_recorder.given([path.toString()]).reply(result).always();
}
return result;
}

@override
Future<Map> post(Uri path, Map<String, String> body) async {
Map result;
if (isRecording) {
return _recording.reply([path.toString(), body]);
} else {
print(path.toString());
result = await _recordAuth.post(path, body);
_recorder.given([path.toString(), body]).reply(result).always();
}
return result;
}

@override
bool get isValid {
return _recordAuth?.isValid ?? true;
}

/// Writes the recorded Reddit API requests and their corresponding responses
/// to [recordingPath] and returns a [Future<File>], which is the file that
/// has been written to, when in recording mode. When not in recording mode,
/// does nothing and returns null.
Future<File> writeRecording() {
if (!isRecording) {
return (new File(_recordingPath)).writeAsString(JSON
.encode(_recorder.toRecording().toJsonEncodable(
encodeRequest: (q) => q, encodeResponse: (r) => r))
.toString());
}
return null;
}
}

0 comments on commit dff27f2

Please sign in to comment.