From 16121e21f9f0b34555f4ef24183949d832e3983d Mon Sep 17 00:00:00 2001 From: Zachary Drayer Date: Wed, 27 Apr 2016 12:51:25 -0700 Subject: [PATCH 1/4] Document the behavior of `-[PF_Twitter init]` It is useful to be able to create a standalone instance of `PFTwitter` to make authenticated Twitter API calls without having to link a Twitter account to a Parse account or require Twitter login. --- ParseTwitterUtils/PF_Twitter.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ParseTwitterUtils/PF_Twitter.h b/ParseTwitterUtils/PF_Twitter.h index 02b16bb..6ad3643 100644 --- a/ParseTwitterUtils/PF_Twitter.h +++ b/ParseTwitterUtils/PF_Twitter.h @@ -21,6 +21,15 @@ NS_ASSUME_NONNULL_BEGIN */ @interface PF_Twitter : NSObject +/** + An instance of `PF_Twitter` configured to access device Twitter accounts with Accounts.framework, + and remote Twitter accounts - if no accounts are found locally - through a built-in webview. + + After setting `consumerKey` and `consumerSecret`, authorization to Twitter accounts can be requested with +`authorizeInBackground`, and then revoked with its opposite, `deauthorizeInBackground`. + */ +- (instancetype)init; + /** Consumer key of the application that is used to authorize with Twitter. */ From 6e5e9ca6381f15428bfc579ce201143ab631b9b7 Mon Sep 17 00:00:00 2001 From: Zachary Drayer Date: Wed, 27 Apr 2016 12:51:43 -0700 Subject: [PATCH 2/4] Add support for deauthorizing from Twitter to invalidate session tokens --- ParseTwitterUtils/PF_Twitter.h | 8 +++++ ParseTwitterUtils/PF_Twitter.m | 44 ++++++++++++++++++++++++ Tests/Unit/TwitterTests.m | 63 ++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/ParseTwitterUtils/PF_Twitter.h b/ParseTwitterUtils/PF_Twitter.h index 6ad3643..dac4511 100644 --- a/ParseTwitterUtils/PF_Twitter.h +++ b/ParseTwitterUtils/PF_Twitter.h @@ -68,6 +68,14 @@ NS_ASSUME_NONNULL_BEGIN */ - (BFTask *)authorizeInBackground; +/** + Invalidates an OAuth token for the current user, if the Twitter user has granted permission to the + current application. + + @return The task, that encapsulates the work being done. + */ +- (BFTask *)deauthorizeInBackground; + /** Displays an auth dialog and populates the authToken, authTokenSecret, userId, and screenName properties if the Twitter user grants permission to the application. diff --git a/ParseTwitterUtils/PF_Twitter.m b/ParseTwitterUtils/PF_Twitter.m index 24f8a4a..387e867 100644 --- a/ParseTwitterUtils/PF_Twitter.m +++ b/ParseTwitterUtils/PF_Twitter.m @@ -92,6 +92,30 @@ - (BFTask *)authorizeInBackground { return source.task; }]; } +- (BFTask *)deauthorizeInBackground { + if (self.consumerKey.length == 0 || self.consumerSecret.length == 0) { + //TODO: (nlutsenko) This doesn't look right, maybe we should add additional error code? + return [BFTask taskWithError:[NSError errorWithDomain:PFParseErrorDomain code:1 userInfo:nil]]; + } + + return [[self _performDeauthAsync] pftw_continueAsyncWithBlock:^id(BFTask *task) { + BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource]; + dispatch_async(dispatch_get_main_queue(), ^{ + if (task.cancelled) { + [source cancel]; + } else if (!task.error && !task.result) { + source.result = nil; + } else if (task.error) { + [source trySetError:task.error]; + } else if (task.result) { + [self setLoginResultValues:@{}]; + + [source trySetResult:task.result]; + } + }); + return source.task; + }]; +} - (void)authorizeWithSuccess:(void (^)(void))success failure:(void (^)(NSError *error))failure @@ -428,6 +452,26 @@ - (BFTask *)_performWebViewAuthAsync { return source.task; } +- (BFTask *)_performDeauthAsync { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + request.URL = [NSURL URLWithString:@"https://api.twitter.com/oauth2/invalidate_token"]; + request.HTTPMethod = @"POST"; + + [self signRequest:request]; + + BFTaskCompletionSource *taskCompletionSource = [BFTaskCompletionSource taskCompletionSource]; + + [[self.urlSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + [taskCompletionSource trySetError:error]; + } else { + [taskCompletionSource trySetResult:data]; + } + }] resume]; + + return taskCompletionSource.task; +} + ///-------------------------------------- #pragma mark - Sign Request ///-------------------------------------- diff --git a/Tests/Unit/TwitterTests.m b/Tests/Unit/TwitterTests.m index 6100b06..7bf18a0 100644 --- a/Tests/Unit/TwitterTests.m +++ b/Tests/Unit/TwitterTests.m @@ -622,4 +622,67 @@ - (void)testAuthorizeWithoutLocalAccountAndNetworkSuccess { OCMVerifyAll(mockedURLSession); } +- (void)testDeauthorizeLoggedOutAccount { + id store = PFStrictClassMock([ACAccountStore class]); + NSURLSession *session = PFStrictClassMock([NSURLSession class]); + id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:store urlSession:session dialogClass:mockedDialog]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[twitter authorizeInBackground] continueWithBlock:^id(BFTask *task) { + NSError *error = task.error; + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, PFParseErrorDomain); + XCTAssertEqual(error.code, 2); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testDeauthorizeLoggedInAccount { + id store = PFStrictClassMock([ACAccountStore class]); + NSURLSession *session = PFStrictClassMock([NSURLSession class]); + + id mockedStore = PFStrictClassMock([ACAccountStore class]); + id mockedURLSession = PFStrictClassMock([NSURLSession class]); + id mockedOperationQueue = PFStrictClassMock([NSOperationQueue class]); + + id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:store urlSession:session dialogClass:mockedDialog]; + twitter.consumerKey = @"consumer_key"; + twitter.consumerSecret = @"consumer_secret"; + twitter.authToken = @"auth_token"; + twitter.authTokenSecret = @"auth_token_secret"; + twitter.userId = @"user_id"; + twitter.screenName = @"screen_name"; + + __block NSURLSessionDataTaskCompletionHandler completionHandler = nil; + [OCMStub([mockedURLSession dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + NSURLRequest *request = obj; + return [request.URL.lastPathComponent isEqualToString:@"invalidate_token"]; + }] completionHandler:[OCMArg checkWithBlock:^BOOL(id obj) { + completionHandler = obj; + return (obj != nil); + }]]).andDo(^(NSInvocation *invocation) { + completionHandler([NSData data], nil, nil); + }) andReturn:[OCMockObject niceMockForClass:[NSURLSessionDataTask class]]]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[twitter deauthorizeInBackground] continueWithBlock:^id(BFTask *task) { + NSError *error = task.error; + XCTAssertNil(error); + XCTAssertNotNil(task.result); + + XCTAssertNil(twitter.userId); + XCTAssertNil(twitter.screenName); + XCTAssertNil(twitter.userId); + XCTAssertNil(twitter.userId); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + @end From ae4a350c673dc6a588951fe6fd5b5100d928f3fd Mon Sep 17 00:00:00 2001 From: Zachary Drayer Date: Thu, 28 Apr 2016 00:34:37 -0700 Subject: [PATCH 3/4] code review feedback, and fix tests --- ParseTwitterUtils/PF_Twitter.h | 4 ++-- ParseTwitterUtils/PF_Twitter.m | 24 +++++++++++------------- Tests/Unit/TwitterTests.m | 13 ++++--------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/ParseTwitterUtils/PF_Twitter.h b/ParseTwitterUtils/PF_Twitter.h index dac4511..6f0df54 100644 --- a/ParseTwitterUtils/PF_Twitter.h +++ b/ParseTwitterUtils/PF_Twitter.h @@ -22,8 +22,8 @@ NS_ASSUME_NONNULL_BEGIN @interface PF_Twitter : NSObject /** - An instance of `PF_Twitter` configured to access device Twitter accounts with Accounts.framework, - and remote Twitter accounts - if no accounts are found locally - through a built-in webview. + Initializes an instance of `PF_Twitter` configured to access device Twitter accounts with Accounts.framework, + and remote access to Twitter accounts - and, if no accounts are found locally - through a built-in webview. After setting `consumerKey` and `consumerSecret`, authorization to Twitter accounts can be requested with `authorizeInBackground`, and then revoked with its opposite, `deauthorizeInBackground`. diff --git a/ParseTwitterUtils/PF_Twitter.m b/ParseTwitterUtils/PF_Twitter.m index 387e867..79164d7 100644 --- a/ParseTwitterUtils/PF_Twitter.m +++ b/ParseTwitterUtils/PF_Twitter.m @@ -100,19 +100,17 @@ - (BFTask *)deauthorizeInBackground { return [[self _performDeauthAsync] pftw_continueAsyncWithBlock:^id(BFTask *task) { BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource]; - dispatch_async(dispatch_get_main_queue(), ^{ - if (task.cancelled) { - [source cancel]; - } else if (!task.error && !task.result) { - source.result = nil; - } else if (task.error) { - [source trySetError:task.error]; - } else if (task.result) { - [self setLoginResultValues:@{}]; - - [source trySetResult:task.result]; - } - }); + if (task.cancelled) { + [source cancel]; + } else if (!task.error && !task.result) { + source.result = nil; + } else if (task.error) { + [source trySetError:task.error]; + } else if (task.result) { + [self setLoginResultValues:nil]; + + [source trySetResult:task.result]; + } return source.task; }]; } diff --git a/Tests/Unit/TwitterTests.m b/Tests/Unit/TwitterTests.m index 7bf18a0..bd6aa1c 100644 --- a/Tests/Unit/TwitterTests.m +++ b/Tests/Unit/TwitterTests.m @@ -633,7 +633,7 @@ - (void)testDeauthorizeLoggedOutAccount { NSError *error = task.error; XCTAssertNotNil(error); XCTAssertEqualObjects(error.domain, PFParseErrorDomain); - XCTAssertEqual(error.code, 2); + XCTAssertEqual(error.code, 1); [expectation fulfill]; return nil; }]; @@ -641,15 +641,12 @@ - (void)testDeauthorizeLoggedOutAccount { } - (void)testDeauthorizeLoggedInAccount { - id store = PFStrictClassMock([ACAccountStore class]); - NSURLSession *session = PFStrictClassMock([NSURLSession class]); - id mockedStore = PFStrictClassMock([ACAccountStore class]); id mockedURLSession = PFStrictClassMock([NSURLSession class]); id mockedOperationQueue = PFStrictClassMock([NSOperationQueue class]); id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); - PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:store urlSession:session dialogClass:mockedDialog]; + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:mockedStore urlSession:mockedURLSession dialogClass:mockedDialog]; twitter.consumerKey = @"consumer_key"; twitter.consumerSecret = @"consumer_secret"; twitter.authToken = @"auth_token"; @@ -674,10 +671,8 @@ - (void)testDeauthorizeLoggedInAccount { XCTAssertNil(error); XCTAssertNotNil(task.result); - XCTAssertNil(twitter.userId); - XCTAssertNil(twitter.screenName); - XCTAssertNil(twitter.userId); - XCTAssertNil(twitter.userId); + XCTAssertNil(twitter.authToken); + XCTAssertNil(twitter.authTokenSecret); [expectation fulfill]; return nil; From 9f062c300a917d4ce94d9b6a150938545400dd66 Mon Sep 17 00:00:00 2001 From: Zachary Drayer Date: Thu, 28 Apr 2016 01:00:40 -0700 Subject: [PATCH 4/4] Re-add nil XCTAsserts for userId and screenName during deauth test --- Tests/Unit/TwitterTests.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/Unit/TwitterTests.m b/Tests/Unit/TwitterTests.m index bd6aa1c..6469cee 100644 --- a/Tests/Unit/TwitterTests.m +++ b/Tests/Unit/TwitterTests.m @@ -673,6 +673,8 @@ - (void)testDeauthorizeLoggedInAccount { XCTAssertNil(twitter.authToken); XCTAssertNil(twitter.authTokenSecret); + XCTAssertNil(twitter.userId); + XCTAssertNil(twitter.screenName); [expectation fulfill]; return nil;