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

Added testing utilities used for recording and replaying Reddit API interactions #7

Merged
merged 4 commits into from
Aug 4, 2017
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
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;
}
}