├── .gitignore ├── .slather.yml ├── .travis.yml ├── CONTRIBUTING.md ├── Cartfile ├── Cartfile.resolved ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Podfile ├── Podfile.lock ├── README.md ├── Rakefile ├── VENCore.podspec ├── VENCore.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── VENCore.xcscheme ├── VENCore ├── Categories │ ├── NSArray+VENCore.h │ ├── NSArray+VENCore.m │ ├── NSDictionary+VENCore.h │ ├── NSDictionary+VENCore.m │ ├── NSError+VENCore.h │ ├── NSError+VENCore.m │ ├── NSString+VENCore.h │ ├── NSString+VENCore.m │ ├── UIDevice+VENCore.h │ └── UIDevice+VENCore.m ├── Info.plist ├── Models │ ├── Transactions │ │ ├── VENCreateTransactionRequest.h │ │ ├── VENCreateTransactionRequest.m │ │ ├── VENTransaction.h │ │ ├── VENTransaction.m │ │ ├── VENTransactionPayloadKeys.h │ │ ├── VENTransactionTarget.h │ │ └── VENTransactionTarget.m │ └── Users │ │ ├── VENUser.h │ │ ├── VENUser.m │ │ └── VENUserPayloadKeys.h ├── Networking │ ├── VENHTTP.h │ ├── VENHTTP.m │ ├── VENHTTPResponse.h │ └── VENHTTPResponse.m ├── VENCore-Prefix.pch ├── VENCore.h └── VENCore.m ├── VENCoreIntegrationTests ├── PaymentSandboxSpec.m ├── VENCoreIntegrationTests-Info.plist ├── VENCoreIntegrationTests-Prefix.pch ├── VENUserIntegrationSpec.m └── en.lproj │ └── InfoPlist.strings ├── VENCoreUnitTests ├── API │ ├── errors │ │ ├── invalidAccessTokenError.json │ │ ├── invalidAmountError.json │ │ └── invalidAudienceError.json │ ├── payments │ │ ├── paymentToEmail.json │ │ └── paymentToUser.json │ └── users │ │ ├── fetchChrisUser.json │ │ ├── fetchFriends.json │ │ ├── fetchInvalidFriends.json │ │ └── fetchInvalidUser.json ├── Categories │ ├── NSArray+VENCoreSpec.m │ ├── NSDictionary+VENCoreSpec.m │ ├── NSError+VENCoreSpec.m │ └── NSString+VENCoreSpec.m ├── Models │ ├── Transactions │ │ ├── VENCreateTransactionRequestSpec.m │ │ ├── VENTransactionSpec.m │ │ └── VENTransactionTargetSpec.m │ └── Users │ │ └── VENUserSpec.m ├── Networking │ ├── VENHTTPResponseSpec.m │ └── VENHTTPSpec.m ├── Utilities │ ├── EXPMatchers+Venmo.h │ ├── EXPMatchers+Venmo.m │ ├── VENTestUtilities.h │ └── VENTestUtilities.m ├── VENCoreSpec.m ├── VENCoreUnitTests-Info.plist ├── VENCoreUnitTests-Prefix.pch └── en.lproj │ └── InfoPlist.strings └── docs └── DESIGN.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/objective-c 2 | 3 | ### Objective-C ### 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata 22 | 23 | ## Other 24 | *.xccheckout 25 | *.moved-aside 26 | *.xcuserstate 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | 33 | # CocoaPods 34 | # 35 | # We recommend against adding the Pods directory to your .gitignore. However 36 | # you should judge for yourself, the pros and cons are mentioned at: 37 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 38 | # 39 | Pods/ 40 | 41 | # Carthage 42 | Carthage/Checkouts 43 | Carthage/Build 44 | 45 | ### Objective-C Patch ### 46 | *.xcscmblueprint 47 | 48 | # Created by https://www.gitignore.io/api/osx 49 | 50 | ### OSX ### 51 | .DS_Store 52 | .AppleDouble 53 | .LSOverride 54 | 55 | # Icon must end with two \r 56 | Icon 57 | 58 | # Thumbnails 59 | ._* 60 | 61 | # Files that might appear in the root of a volume 62 | .DocumentRevisions-V100 63 | .fseventsd 64 | .Spotlight-V100 65 | .TemporaryItems 66 | .Trashes 67 | .VolumeIcon.icns 68 | 69 | # Directories potentially created on remote AFP share 70 | .AppleDB 71 | .AppleDesktop 72 | Network Trash Folder 73 | Temporary Items 74 | .apdisk 75 | 76 | *.gcno 77 | *.gcda 78 | 79 | VENCore.xcworkspace 80 | -------------------------------------------------------------------------------- /.slather.yml: -------------------------------------------------------------------------------- 1 | coverage_service: coveralls 2 | xcodeproj: VENCore.xcodeproj 3 | ignore: 4 | - VENCoreUnitTests/.* 5 | - VENCoreIntegrationTests/.* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | install: 3 | - bundle install --jobs=3 --retry=3 4 | script: bundle exec rake test 5 | after_success: slather 6 | cache: 7 | directories: 8 | - vendor/bundle 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 1. Make sure the tests pass. 2 | - To run the integration tests (disabled in the scheme by default), you'll need to add a "config.plist" file to the VENCoreIntegrationTests target. This plist should contain an "access_token" key with your Venmo access token as the value. 3 | 4 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "calebd/CMDQueryStringSerialization" ~> 0.4 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "soffes/ISO8601" "v0.3.0" 2 | github "calebd/CMDQueryStringSerialization" "v0.4.3" 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | gem 'cocoapods' 5 | gem 'xcpretty' 6 | gem 'plist' 7 | gem 'slather' 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.2.4) 5 | i18n (~> 0.7) 6 | json (~> 1.7, >= 1.7.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | claide (0.9.1) 11 | clamp (0.6.5) 12 | cocoapods (0.39.0) 13 | activesupport (>= 4.0.2) 14 | claide (~> 0.9.1) 15 | cocoapods-core (= 0.39.0) 16 | cocoapods-downloader (~> 0.9.3) 17 | cocoapods-plugins (~> 0.4.2) 18 | cocoapods-search (~> 0.1.0) 19 | cocoapods-stats (~> 0.6.2) 20 | cocoapods-trunk (~> 0.6.4) 21 | cocoapods-try (~> 0.5.1) 22 | colored (~> 1.2) 23 | escape (~> 0.0.4) 24 | molinillo (~> 0.4.0) 25 | nap (~> 1.0) 26 | xcodeproj (~> 0.28.2) 27 | cocoapods-core (0.39.0) 28 | activesupport (>= 4.0.2) 29 | fuzzy_match (~> 2.0.4) 30 | nap (~> 1.0) 31 | cocoapods-downloader (0.9.3) 32 | cocoapods-plugins (0.4.2) 33 | nap 34 | cocoapods-search (0.1.0) 35 | cocoapods-stats (0.6.2) 36 | cocoapods-trunk (0.6.4) 37 | nap (>= 0.8, < 2.0) 38 | netrc (= 0.7.8) 39 | cocoapods-try (0.5.1) 40 | colored (1.2) 41 | escape (0.0.4) 42 | fuzzy_match (2.0.4) 43 | i18n (0.7.0) 44 | json (1.8.3) 45 | minitest (5.8.2) 46 | molinillo (0.4.0) 47 | nap (1.0.0) 48 | netrc (0.7.8) 49 | plist (3.1.0) 50 | rake (10.4.2) 51 | rouge (1.10.1) 52 | slather (1.3.0) 53 | clamp (~> 0.6) 54 | xcodeproj (~> 0.17) 55 | thread_safe (0.3.5) 56 | tzinfo (1.2.2) 57 | thread_safe (~> 0.1) 58 | xcodeproj (0.28.2) 59 | activesupport (>= 3) 60 | claide (~> 0.9.1) 61 | colored (~> 1.2) 62 | xcpretty (0.2.1) 63 | rouge (~> 1.8) 64 | 65 | PLATFORMS 66 | ruby 67 | 68 | DEPENDENCIES 69 | cocoapods 70 | plist 71 | rake 72 | slather 73 | xcpretty 74 | 75 | BUNDLED WITH 76 | 1.10.6 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Venmo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | platform :ios, '8.0' 4 | 5 | inhibit_all_warnings! 6 | 7 | use_frameworks! 8 | 9 | podspec 10 | 11 | target 'VENCoreUnitTests', :exclusive => true do 12 | pod 'Expecta' 13 | pod 'Nocilla' 14 | pod 'OCMock', '~> 2.2' 15 | pod 'OCHamcrest' 16 | pod 'Specta' 17 | end 18 | 19 | target 'VENCoreIntegrationTests', :exclusive => true do 20 | pod 'Expecta' 21 | pod 'Nocilla' 22 | pod 'OCMock', '~> 2.2' 23 | pod 'OCHamcrest' 24 | pod 'Specta' 25 | end 26 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - CMDQueryStringSerialization (0.4.1): 3 | - ISO8601 4 | - Expecta (1.0.3) 5 | - ISO8601 (0.3.0) 6 | - Nocilla (0.10.0) 7 | - OCHamcrest (4.3.0) 8 | - OCMock (2.2.4) 9 | - Specta (1.0.4) 10 | 11 | DEPENDENCIES: 12 | - CMDQueryStringSerialization (~> 0.4) 13 | - Expecta 14 | - Nocilla 15 | - OCHamcrest 16 | - OCMock (~> 2.2) 17 | - Specta 18 | 19 | SPEC CHECKSUMS: 20 | CMDQueryStringSerialization: 4bb0a2f5e7d8d8678d911d88072274952cbef4e5 21 | Expecta: 9d1bff6c8b0eeee73a166a2ee898892478927a15 22 | ISO8601: 8d8a22d5edf0554a1cf75bac028c76c1dc0ffaef 23 | Nocilla: ae0a2b05f3087b473624ac2b25903695df51246a 24 | OCHamcrest: cd63d27f48a266d4412c0b295b01b8f0940efa81 25 | OCMock: a6a7dc0e3997fb9f35d99f72528698ebf60d64f2 26 | Specta: 69bb134672aae190a1379ff91df07dad8dd1f869 27 | 28 | COCOAPODS: 0.39.0 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | VENCore [![Build Status](https://travis-ci.org/venmo/VENCore.svg?branch=v1.0.0)](https://travis-ci.org/venmo/VENCore) [![Coverage Status](https://coveralls.io/repos/venmo/VENCore/badge.png?branch=master)](https://coveralls.io/r/venmo/VENCore?branch=master) 2 | ======= 3 | 4 | VENCore is the core Objective-C client library for the Venmo API. If you're looking for a simple way to send Venmo payments & charges from your iOS app, you should use the [Venmo iOS SDK](https://github.com/venmo/venmo-ios-sdk). 5 | 6 | ## Usage 7 | 8 | ### Initialization 9 | ```objective-c 10 | // Create a VENCore instance 11 | VENCore *core = [[VENCore alloc] init]; 12 | 13 | // Give it an access token 14 | [core setAccessToken:accessToken]; 15 | 16 | // Set the default core 17 | [VENCore setDefaultCore:core]; 18 | ``` 19 | 20 | ### Sending a transaction 21 | ```objective-c 22 | // Create a request 23 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 24 | 25 | // Add a note 26 | transactionService.note = @"hi"; 27 | 28 | // Add a target 29 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"name@example.com" amount:30]; 30 | [transactionService addTransactionTarget:target]; 31 | 32 | // Send the request 33 | [transactionService sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 34 | // :) 35 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 36 | // :( 37 | }]; 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | WORKSPACE = 'VENCore.xcworkspace' 2 | SCHEME = 'VENCore' 3 | 4 | desc 'Run the tests' 5 | task :test do 6 | require_binary 'bundle', 'gem install bundler' 7 | sh 'bundle exec pod install' 8 | 9 | require_binary 'xcodebuild', 'brew install xcodebuild' 10 | require_binary 'xcpretty', 'bundle install' 11 | sh "xcodebuild test -workspace #{WORKSPACE} -scheme #{SCHEME} -destination 'platform=iOS Simulator,name=iPhone 4s,OS=latest' | bundle exec xcpretty --color; exit ${PIPESTATUS[0]}" 12 | end 13 | 14 | task :default => :test 15 | 16 | desc 'Print test coverage of the last test run.' 17 | task :coverage do 18 | require_binary 'slather', 'bundle install' 19 | sh 'slather coverage -s' 20 | end 21 | 22 | 23 | private 24 | 25 | def require_binary(binary, install) 26 | if `which #{binary}`.length == 0 27 | fail "\nERROR: #{binary} isn't installed. Please install #{binary} with the following command:\n\n $ #{install}\n\n" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /VENCore.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'VENCore' 3 | s.version = '3.1.3' 4 | s.summary = 'Core Venmo client library' 5 | s.description = 'Core iOS client library for the Venmo api' 6 | s.homepage = 'https://github.com/venmo/VENCore' 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.author = { 'Venmo' => 'ios@venmo.com' } 9 | s.platform = :ios, '7.1' 10 | s.source = { :git => 'https://github.com/venmo/VENCore.git', 11 | :tag => "v#{s.version}" } 12 | s.source_files = 'VENCore/**/*.{h,m}' 13 | s.dependency 'CMDQueryStringSerialization', '~> 0.4' 14 | end 15 | -------------------------------------------------------------------------------- /VENCore.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /VENCore.xcodeproj/xcshareddata/xcschemes/VENCore.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 93 | 99 | 100 | 101 | 102 | 103 | 104 | 110 | 111 | 117 | 118 | 119 | 120 | 122 | 123 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /VENCore/Categories/NSArray+VENCore.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @interface NSMutableArray (VENCore) 4 | 5 | /** 6 | * Removes all elements which have NULL values from the array 7 | */ 8 | - (void)cleanseResponseArray; 9 | 10 | @end 11 | 12 | @interface NSArray (VENCore) 13 | /** 14 | * Returns an array containing all non-Null elements from the receiving array 15 | * @return Array with no NULL values 16 | */ 17 | - (instancetype)arrayByCleansingResponseArray; 18 | @end 19 | -------------------------------------------------------------------------------- /VENCore/Categories/NSArray+VENCore.m: -------------------------------------------------------------------------------- 1 | #import "NSArray+VENCore.h" 2 | #import "NSDictionary+VENCore.h" 3 | 4 | @implementation NSMutableArray (VENCore) 5 | 6 | - (void)cleanseResponseArray 7 | { 8 | NSMutableArray *elementsToRemoveArray = [[NSMutableArray alloc] initWithCapacity:[self count]]; 9 | for (NSUInteger i=0; i<[self count]; i++) { 10 | if ([self[i] isKindOfClass:[NSNumber class]]) { 11 | self[i] = [(NSNumber *)self[i] stringValue]; 12 | } 13 | else if (self[i] == [NSNull null]) { 14 | [elementsToRemoveArray addObject:self[i]]; 15 | } 16 | else if ([self[i] isKindOfClass:[NSDictionary class]]) { 17 | self[i] = [(NSMutableDictionary *)self[i] dictionaryByCleansingResponseDictionary]; 18 | } 19 | else if ([self[i] isKindOfClass:[NSArray class]]) { 20 | self[i] = [(NSArray *)self[i] arrayByCleansingResponseArray]; 21 | } 22 | } 23 | for (NSObject *objectToRemove in elementsToRemoveArray) { 24 | [self removeObject:objectToRemove]; 25 | } 26 | } 27 | 28 | @end 29 | 30 | @implementation NSArray (VENCore) 31 | 32 | - (instancetype)arrayByCleansingResponseArray 33 | { 34 | NSMutableArray *array = [self mutableCopy]; 35 | [array cleanseResponseArray]; 36 | return [NSArray arrayWithArray:array]; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /VENCore/Categories/NSDictionary+VENCore.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @interface NSMutableDictionary (VENCore) 4 | 5 | /** 6 | * Removes all keys which have NULL values from the dictionary 7 | */ 8 | - (void)cleanseResponseDictionary; 9 | 10 | @end 11 | 12 | @interface NSDictionary (VENCore) 13 | 14 | /** 15 | * Returns the object associated with the given key 16 | * @param key An object identifying the value 17 | * @return The value associated with key, or nil if no value is associated with the key. 18 | */ 19 | - (id)objectOrNilForKey:(id)key; 20 | 21 | 22 | /** 23 | * Returns the bool value associated with the given key 24 | * @param key An object identifying the value 25 | * @return The bool value associated with the key, or nil if no bool value can be associated with the key. 26 | */ 27 | - (BOOL)boolForKey:(id)key; 28 | 29 | /** 30 | * Returns the string value associated with the given key 31 | * @param key An object identifying the value 32 | * @return The string value associated with the key, or nil if no string value can be associated 33 | * with the key. 34 | */ 35 | - (NSString *)stringForKey:(id)key; 36 | 37 | /** 38 | * Returns a dictionary containing all non-Null key-value pairs from the receiving dictionary 39 | * @return Dictionary with no NULL values 40 | */ 41 | - (instancetype)dictionaryByCleansingResponseDictionary; 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /VENCore/Categories/NSDictionary+VENCore.m: -------------------------------------------------------------------------------- 1 | #import "NSDictionary+VENCore.h" 2 | #import "NSArray+VENCore.h" 3 | 4 | @implementation NSMutableDictionary (VENCore) 5 | 6 | - (void)cleanseResponseDictionary { 7 | for (NSString *key in [self allKeys]) { 8 | NSObject *object = (NSObject *) self[key]; 9 | if (object == [NSNull null]) { 10 | [self removeObjectForKey:key]; 11 | } 12 | else if ([object isKindOfClass:[NSNumber class]]) { 13 | self[key] = [((NSNumber *)object) stringValue]; 14 | } 15 | else if ([object isKindOfClass:[NSDictionary class]]) { 16 | self[key] = [((NSDictionary *)object) dictionaryByCleansingResponseDictionary]; 17 | } 18 | else if([object isKindOfClass:[NSArray class]]) { 19 | NSArray *array = [(NSArray *)object copy]; 20 | self[key] = [array arrayByCleansingResponseArray]; 21 | } 22 | } 23 | } 24 | 25 | @end 26 | 27 | @implementation NSDictionary (VENCore) 28 | 29 | - (id)objectOrNilForKey:(id)key { 30 | id object = self[key]; 31 | return object == [NSNull null] ? nil : object; 32 | } 33 | 34 | 35 | - (BOOL)boolForKey:(id)key { 36 | id object = [self objectOrNilForKey:key]; 37 | if ([object respondsToSelector:@selector(boolValue)]) { 38 | return [object boolValue]; 39 | } else { 40 | return object != nil; 41 | } 42 | } 43 | 44 | 45 | - (NSString *)stringForKey:(id)key { 46 | id object = [self objectOrNilForKey:key]; 47 | return [object respondsToSelector:@selector(stringValue)] ? [object stringValue] : object; 48 | } 49 | 50 | 51 | - (instancetype)dictionaryByCleansingResponseDictionary { 52 | 53 | NSMutableDictionary *dictionary = [self mutableCopy]; 54 | [dictionary cleanseResponseDictionary]; 55 | 56 | return [NSDictionary dictionaryWithDictionary:dictionary]; 57 | } 58 | 59 | @end 60 | 61 | -------------------------------------------------------------------------------- /VENCore/Categories/NSError+VENCore.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @interface NSError (VENCore) 4 | 5 | /** 6 | * Returns an NSError object with the given domain, code, description, and recovery suggestion. 7 | * @param code The error code 8 | * @param description A description of the error 9 | * @param recoverySuggestion A description of how to recover from the error 10 | */ 11 | + (instancetype)errorWithDomain:(NSString *)domain 12 | code:(NSInteger)code 13 | description:(NSString *)description 14 | recoverySuggestion:(NSString *)recoverySuggestion; 15 | 16 | /** 17 | * Returns the default error for failing VENHTTP responses. 18 | */ 19 | + (instancetype)defaultResponseError; 20 | 21 | 22 | /** 23 | * Returns an error indicating that no default core has been set. 24 | */ 25 | + (instancetype)noDefaultCoreError; 26 | 27 | 28 | /** 29 | * Returns an error indicating that the default VENCore instance 30 | * doesn't have an access token. 31 | */ 32 | + (instancetype)noAccessTokenError; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /VENCore/Categories/NSError+VENCore.m: -------------------------------------------------------------------------------- 1 | #import "NSError+VENCore.h" 2 | #import "VENHTTPResponse.h" 3 | #import "VENCore.h" 4 | 5 | @implementation NSError (VENCore) 6 | 7 | + (instancetype)errorWithDomain:(NSString *)domain 8 | code:(NSInteger)code 9 | description:(NSString *)description 10 | recoverySuggestion:(NSString *)recoverySuggestion { 11 | 12 | NSDictionary *errorUserInfo = 13 | [NSDictionary dictionaryWithObjectsAndKeys: 14 | NSLocalizedString(description, nil), NSLocalizedDescriptionKey, 15 | NSLocalizedString(recoverySuggestion, nil), NSLocalizedRecoverySuggestionErrorKey, nil]; 16 | 17 | return [self errorWithDomain:domain code:code userInfo:errorUserInfo]; 18 | 19 | } 20 | 21 | 22 | + (instancetype)defaultResponseError { 23 | return [self errorWithDomain:VENErrorDomainHTTPResponse 24 | code:VENErrorCodeHTTPResponseBadResponse 25 | description:NSLocalizedString(@"Bad response", nil) 26 | recoverySuggestion:nil]; 27 | } 28 | 29 | 30 | + (instancetype)noDefaultCoreError { 31 | return [self errorWithDomain:VENErrorDomainCore 32 | code:VENCoreErrorCodeNoDefaultCore 33 | description:NSLocalizedString(@"No default core", nil) 34 | recoverySuggestion:NSLocalizedString(@"Use setDefaultCore to set the default VENCore instance.", nil)]; 35 | } 36 | 37 | 38 | + (instancetype)noAccessTokenError { 39 | return [self errorWithDomain:VENErrorDomainCore 40 | code:VENCoreErrorCodeNoAccessToken 41 | description:NSLocalizedString(@"No access token", nil) 42 | recoverySuggestion:NSLocalizedString(@"Set the default core's access token.", nil)]; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /VENCore/Categories/NSString+VENCore.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | #import 4 | 5 | @interface NSString (VENCore) 6 | 7 | - (BOOL)isUSPhone; 8 | - (BOOL)isEmail; 9 | - (BOOL)isUserId; 10 | - (VENTargetType)targetType; 11 | - (BOOL)hasContent; 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /VENCore/Categories/NSString+VENCore.m: -------------------------------------------------------------------------------- 1 | #import "NSString+VENCore.h" 2 | 3 | @implementation NSString (VENCore) 4 | 5 | - (BOOL)isUSPhone { 6 | NSString *phoneRegex = @"^\\+?1?\\D{0,2}(\\d{3})\\D{0,2}\\D?(\\d{3})\\D?(\\d{4})$"; 7 | NSPredicate *phoneTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", phoneRegex]; 8 | return [phoneTest evaluateWithObject:self]; 9 | } 10 | 11 | 12 | - (BOOL)isEmail { 13 | NSString *lowerCaseSelf = [self lowercaseString]; 14 | NSString *pattern = @"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"; 15 | NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", pattern]; 16 | return [predicate evaluateWithObject:lowerCaseSelf]; 17 | } 18 | 19 | 20 | - (BOOL)isUserId { 21 | NSCharacterSet *notDigitSet = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet]; 22 | return ![self isUSPhone] && 23 | [self rangeOfCharacterFromSet:notDigitSet].location == NSNotFound; 24 | } 25 | 26 | 27 | - (VENTargetType)targetType { 28 | if ([self isUSPhone]) { 29 | return VENTargetTypePhone; 30 | } 31 | else if ([self isEmail]) { 32 | return VENTargetTypeEmail; 33 | } 34 | else if ([self isUserId]) { 35 | return VENTargetTypeUserId; 36 | } 37 | else { 38 | return VENTargetTypeUnknown; 39 | } 40 | } 41 | 42 | 43 | - (BOOL)hasContent { 44 | NSCharacterSet *set = [NSCharacterSet whitespaceCharacterSet]; 45 | if ([[self stringByTrimmingCharactersInSet: set] length] == 0) 46 | { 47 | return NO; 48 | } 49 | return YES; 50 | } 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /VENCore/Categories/UIDevice+VENCore.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | 3 | @interface UIDevice (VENCore) 4 | 5 | - (NSString *)VEN_platformString; 6 | 7 | - (NSString *)VEN_deviceIDString; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /VENCore/Categories/UIDevice+VENCore.m: -------------------------------------------------------------------------------- 1 | #import "UIDevice+VENCore.h" 2 | #import 3 | 4 | NSString *const VENUserDefaultsKeyDeviceID = @"VenmoDeviceID"; 5 | 6 | @implementation UIDevice (VENCore) 7 | 8 | 9 | - (NSString *)VEN_platformString { 10 | size_t size; 11 | sysctlbyname("hw.machine", NULL, &size, NULL, 0); 12 | char *machine = malloc(size); 13 | sysctlbyname("hw.machine", machine, &size, NULL, 0); 14 | NSString *platform = [NSString stringWithCString:machine encoding:NSUTF8StringEncoding]; 15 | free(machine); 16 | return platform; 17 | } 18 | 19 | 20 | - (NSString *)VEN_deviceIDString { 21 | NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 22 | NSString *uniqueIdentifier = [userDefaults stringForKey:VENUserDefaultsKeyDeviceID]; 23 | if (!uniqueIdentifier) { 24 | CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault); 25 | uniqueIdentifier = CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, uuid)); 26 | CFRelease(uuid); 27 | [userDefaults setObject:uniqueIdentifier forKey:VENUserDefaultsKeyDeviceID]; 28 | [userDefaults synchronize]; 29 | } 30 | return uniqueIdentifier; 31 | } 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /VENCore/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /VENCore/Models/Transactions/VENCreateTransactionRequest.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | #import 4 | 5 | @interface VENCreateTransactionRequest : NSObject 6 | 7 | @property (strong, nonatomic, readonly) NSOrderedSet *targets; 8 | @property (strong, nonatomic) NSString *note; 9 | @property (assign, nonatomic) VENTransactionType transactionType; 10 | @property (assign, nonatomic) VENTransactionAudience audience; 11 | 12 | /** 13 | * Sends transactions from the current user to all added targets. 14 | * @param successBlock The block to be executed after transactions are successfully sent to all targets. This block has no return value and takes two arguments: an array of the VENTransaction objects returned by the sent transactions, and the response from the last transaction. 15 | * @param failureBlock The block to be executed after a transaction fails. This block has no return value and takes three arguments: an array of the VENTransaction objects returned by the sent transactions, the response from the transaction that failed to send, and an error object. 16 | */ 17 | - (void)sendWithSuccess:(void(^)(NSArray *sentTransactions, 18 | VENHTTPResponse *response))successBlock 19 | failure:(void(^)(NSArray *sentTransactions, 20 | VENHTTPResponse *response, 21 | NSError *error))failureBlock; 22 | /** 23 | * Adds a transaction target 24 | * @note If the target is invalid or a duplicate, addTarget: will return NO 25 | * and no target will be added to the transaction. 26 | * @return Returns a Boolean value indicating whether the target was successfully added. 27 | */ 28 | - (BOOL)addTransactionTarget:(VENTransactionTarget *)target; 29 | 30 | /** 31 | * Indicates whether the transaction service is valid and ready to send transactions to its current targets 32 | */ 33 | - (BOOL)readyToSend; 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /VENCore/Models/Transactions/VENCreateTransactionRequest.m: -------------------------------------------------------------------------------- 1 | #import "VENCreateTransactionRequest.h" 2 | #import "VENCore.h" 3 | #import "NSString+VENCore.h" 4 | #import "NSError+VENCore.h" 5 | #import "NSDictionary+VENCore.h" 6 | 7 | @import CoreGraphics; 8 | 9 | @interface VENCreateTransactionRequest () 10 | 11 | @property (nonatomic, strong) NSMutableOrderedSet *mutableTargets; 12 | 13 | @end 14 | 15 | 16 | @implementation VENCreateTransactionRequest 17 | 18 | - (id)init { 19 | self = [super init]; 20 | if (self) { 21 | self.mutableTargets = [[NSMutableOrderedSet alloc] init]; 22 | } 23 | return self; 24 | } 25 | 26 | - (BOOL)readyToSend { 27 | if (![self.mutableTargets count] || 28 | ![self.note hasContent] || 29 | self.transactionType == VENTransactionTypeUnknown) { 30 | return NO; 31 | } 32 | return YES; 33 | } 34 | 35 | - (void)sendWithSuccess:(void(^)(NSArray *sentTransactions, 36 | VENHTTPResponse *response))successBlock 37 | failure:(void(^)(NSArray *sentTransactions, 38 | VENHTTPResponse *response, 39 | NSError *error))failureBlock { 40 | [self sendTargets:[self.targets mutableCopy] 41 | sentTransactions:nil 42 | withSuccess:successBlock 43 | failure:failureBlock]; 44 | } 45 | 46 | 47 | - (void)sendTargets:(NSMutableOrderedSet *)targets 48 | sentTransactions:(NSMutableArray *)sentTransactions 49 | withSuccess:(void (^)(NSArray *sentTransactions, 50 | VENHTTPResponse *response))successBlock 51 | failure:(void(^)(NSArray *sentTransactions, 52 | VENHTTPResponse *response, 53 | NSError *error))failureBlock { 54 | if (!sentTransactions) { 55 | sentTransactions = [[NSMutableArray alloc] init]; 56 | } 57 | 58 | if ([targets count] == 0) { 59 | if (successBlock) { 60 | successBlock(sentTransactions, nil); 61 | } 62 | return; 63 | } 64 | 65 | VENTransactionTarget *target = [targets firstObject]; 66 | [targets removeObjectAtIndex:0]; 67 | NSString *accessToken = [VENCore defaultCore].accessToken; 68 | if (!accessToken) { 69 | failureBlock(nil, nil, [NSError noAccessTokenError]); 70 | return; 71 | } 72 | NSMutableDictionary *postParameters = [NSMutableDictionary dictionaryWithDictionary:@{@"access_token" : accessToken}]; 73 | [postParameters addEntriesFromDictionary:[self dictionaryWithParametersForTarget:target]]; 74 | [[VENCore defaultCore].httpClient POST:VENAPIPathPayments 75 | parameters:postParameters 76 | success:^(VENHTTPResponse *response) { 77 | NSDictionary *data = [response.object objectOrNilForKey:@"data"]; 78 | NSDictionary *payment = [data objectOrNilForKey:@"payment"]; 79 | VENTransaction *newTransaction; 80 | if (payment) { 81 | newTransaction = [[VENTransaction alloc] initWithDictionary:payment]; 82 | } 83 | [sentTransactions addObject:newTransaction]; 84 | 85 | [self sendTargets:targets 86 | sentTransactions:sentTransactions 87 | withSuccess:successBlock 88 | failure:failureBlock]; 89 | } 90 | failure:^(VENHTTPResponse *response, NSError *error) { 91 | if (failureBlock) { 92 | failureBlock(sentTransactions, response, error); 93 | } 94 | }]; 95 | } 96 | 97 | 98 | - (BOOL)addTransactionTarget:(VENTransactionTarget *)target { 99 | 100 | if (![target isKindOfClass:[VENTransactionTarget class]] 101 | || ![target isValid] 102 | || [self containsDuplicateOfTarget:target]) { 103 | return NO; 104 | } 105 | 106 | [self.mutableTargets addObject:target]; 107 | return YES; 108 | } 109 | 110 | 111 | #pragma mark - Other Methods 112 | 113 | - (NSOrderedSet *)targets { 114 | return [self.mutableTargets copy]; 115 | } 116 | 117 | - (NSDictionary *)dictionaryWithParametersForTarget:(VENTransactionTarget *)target { 118 | NSString *recipientTypeKey; 119 | NSString *audienceString; 120 | NSString*amountString; 121 | switch (target.targetType) { 122 | case VENTargetTypeEmail: 123 | recipientTypeKey = @"email"; 124 | break; 125 | case VENTargetTypePhone: 126 | recipientTypeKey = @"phone"; 127 | break; 128 | case VENTargetTypeUserId: 129 | recipientTypeKey = @"user_id"; 130 | break; 131 | default: 132 | return nil; 133 | break; 134 | } 135 | 136 | switch (self.audience) { 137 | case VENTransactionAudienceFriends: 138 | audienceString = @"friends"; 139 | break; 140 | case VENTransactionAudiencePublic: 141 | audienceString = @"public"; 142 | break; 143 | default: 144 | audienceString = @"private"; 145 | break; 146 | } 147 | CGFloat dollarAmount = (CGFloat)target.amount/100.; 148 | amountString = [NSString stringWithFormat:@"%.2f", dollarAmount]; 149 | if (self.transactionType == VENTransactionTypeCharge) { 150 | amountString = [@"-" stringByAppendingString:amountString]; 151 | } 152 | NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary: 153 | @{recipientTypeKey: target.handle, 154 | @"note": self.note, 155 | @"amount": amountString}]; 156 | 157 | if (self.audience != VENTransactionAudienceUserDefault) { 158 | [parameters addEntriesFromDictionary:@{@"audience": audienceString}]; 159 | } 160 | 161 | return parameters; 162 | } 163 | 164 | 165 | - (BOOL)containsDuplicateOfTarget:(VENTransactionTarget *)target { 166 | NSString *handle = target.handle; 167 | for (VENTransactionTarget *currentTarget in self.targets) { 168 | if ([handle isEqualToString:currentTarget.handle]) { 169 | return YES; 170 | } 171 | } 172 | return NO; 173 | } 174 | 175 | @end 176 | -------------------------------------------------------------------------------- /VENCore/Models/Transactions/VENTransaction.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @class VENMutableTransaction, VENUser, VENHTTPResponse, VENTransactionTarget; 4 | 5 | typedef NS_ENUM(NSUInteger, VENTransactionType) { 6 | VENTransactionTypeUnknown, 7 | VENTransactionTypePay, 8 | VENTransactionTypeCharge 9 | }; 10 | extern NSString *const VENTransactionTypeStrings[]; 11 | 12 | typedef NS_ENUM(NSUInteger, VENTransactionStatus) { 13 | VENTransactionStatusUnknown, 14 | VENTransactionStatusPending, 15 | VENTransactionStatusSettled, 16 | VENTransactionStatusFailed 17 | }; 18 | extern NSString *const VENTransactionStatusStrings[]; 19 | 20 | typedef NS_ENUM(NSUInteger, VENTransactionAudience) { 21 | // Indicates that the transaction uses/should use the user's default sharing setting 22 | VENTransactionAudienceUserDefault, 23 | VENTransactionAudiencePrivate, 24 | VENTransactionAudienceFriends, 25 | VENTransactionAudiencePublic 26 | }; 27 | extern NSString *const VENTransactionAudienceStrings[]; 28 | 29 | extern NSString *const VENErrorDomainTransaction; 30 | 31 | typedef NS_ENUM(NSUInteger, VENErrorCodeTransaction) { 32 | VENErrorCodeTransactionDuplicateTarget, 33 | VENErrorCodeTransactionInvalidTarget 34 | }; 35 | 36 | @interface VENTransaction : NSObject 37 | 38 | @property (copy, nonatomic, readonly) NSString *transactionID; 39 | @property (strong, nonatomic, readonly) VENTransactionTarget *target; 40 | @property (copy, nonatomic, readonly) NSString *note; 41 | @property (copy, nonatomic, readonly) VENUser *actor; 42 | @property (assign, nonatomic, readonly) VENTransactionType transactionType; 43 | @property (assign, nonatomic, readonly) VENTransactionStatus status; 44 | @property (assign, nonatomic, readonly) VENTransactionAudience audience; 45 | 46 | + (BOOL)canInitWithDictionary:(NSDictionary *)dictionary; 47 | 48 | /** 49 | * Creates a VENTransaction from a dictionary representation 50 | * @note should call canInitWithDictionary first 51 | * @return Returns an instance of VENTransaction 52 | */ 53 | - (instancetype)initWithDictionary:(NSDictionary *)dictionary; 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /VENCore/Models/Transactions/VENTransaction.m: -------------------------------------------------------------------------------- 1 | #import "VENTransaction.h" 2 | #import "VENCore.h" 3 | #import "VENTransactionPayloadKeys.h" 4 | #import "NSDictionary+VENCore.h" 5 | #import "VENTransactionTarget.h" 6 | 7 | NSString *const VENErrorDomainTransaction = @"com.venmo.VENCore.ErrorDomain.VENTransaction"; 8 | NSString *const VENTransactionTypeStrings[] = {@"unknown", @"pay", @"charge"}; 9 | NSString *const VENTransactionStatusStrings[] = {@"not_sent", @"pending", @"settled", @"failed"}; 10 | NSString *const VENTransactionAudienceStrings[] = {@"default", @"private", @"friends", @"public"}; 11 | 12 | @interface VENTransaction () 13 | 14 | @property (copy, nonatomic, readwrite) NSString *transactionID; 15 | @property (strong, nonatomic, readwrite) VENTransactionTarget *target; 16 | @property (copy, nonatomic, readwrite) NSString *note; 17 | @property (copy, nonatomic, readwrite) VENUser *actor; 18 | @property (assign, nonatomic, readwrite) VENTransactionType transactionType; 19 | @property (assign, nonatomic, readwrite) VENTransactionStatus status; 20 | @property (assign, nonatomic, readwrite) VENTransactionAudience audience; 21 | 22 | @end 23 | 24 | @implementation VENTransaction 25 | 26 | #pragma mark - Class Methods 27 | 28 | + (BOOL)canInitWithDictionary:(NSDictionary *)dictionary { 29 | NSArray *requiredKeys = @[VENTransactionAmountKey, VENTransactionNoteKey, VENTransactionActorKey, VENTransactionIDKey, VENTransactionTargetKey]; 30 | for (NSString *key in requiredKeys) { 31 | if (!dictionary[key] || [dictionary[key] isKindOfClass:[NSNull class]] 32 | || ([dictionary[key] respondsToSelector:@selector(isEqualToString:)] 33 | && [dictionary[key] isEqualToString:@""])) { 34 | return NO; 35 | } 36 | } 37 | return YES; 38 | } 39 | 40 | #pragma mark - Public Instance Methods 41 | 42 | - (instancetype)initWithDictionary:(NSDictionary *)dictionary { 43 | 44 | self = [super init]; 45 | 46 | if (self) { 47 | if (!dictionary || ![dictionary isKindOfClass:[NSDictionary class]]) { 48 | return self; 49 | } 50 | 51 | NSDictionary *cleanDictionary = [dictionary dictionaryByCleansingResponseDictionary]; 52 | 53 | // Main Transaction Body 54 | self.transactionID = cleanDictionary[VENTransactionIDKey]; 55 | self.note = cleanDictionary[VENTransactionNoteKey]; 56 | 57 | NSString *transactionType = cleanDictionary[VENTransactionTypeKey]; 58 | NSString *transactionStatus = cleanDictionary[VENTransactionStatusKey]; 59 | NSString *transactionAudience = cleanDictionary[VENTransactionAudienceKey]; 60 | 61 | 62 | // Set transaction type enumeration 63 | if ([transactionType isEqualToString:VENTransactionTypeStrings[VENTransactionTypeCharge]]) { 64 | self.transactionType = VENTransactionTypeCharge; 65 | } 66 | else if ([transactionType isEqualToString:VENTransactionTypeStrings[VENTransactionTypePay]]) { 67 | self.transactionType = VENTransactionTypePay; 68 | } 69 | else { 70 | self.transactionType = VENTransactionTypeUnknown; 71 | } 72 | 73 | 74 | // Set status enumeration 75 | if ([transactionStatus isEqualToString:VENTransactionStatusStrings[VENTransactionStatusPending]]) { 76 | self.status = VENTransactionStatusPending; 77 | } 78 | else if ([transactionStatus isEqualToString:VENTransactionStatusStrings[VENTransactionStatusSettled]]) { 79 | self.status = VENTransactionStatusSettled; 80 | } 81 | else if ([transactionStatus isEqualToString:VENTransactionStatusStrings[VENTransactionStatusFailed]]) { 82 | self.status = VENTransactionStatusFailed; 83 | } 84 | else { 85 | self.status = VENTransactionStatusUnknown; 86 | } 87 | 88 | 89 | // Set audience enumeration 90 | if ([transactionAudience isEqualToString:VENTransactionAudienceStrings[VENTransactionAudiencePublic]]) { 91 | self.audience = VENTransactionAudiencePublic; 92 | } 93 | else if ([transactionAudience isEqualToString:VENTransactionAudienceStrings[VENTransactionAudienceFriends]]) { 94 | self.audience = VENTransactionAudienceFriends; 95 | } 96 | else if ([transactionAudience isEqualToString:VENTransactionAudienceStrings[VENTransactionAudiencePrivate]]) { 97 | self.audience = VENTransactionAudiencePrivate; 98 | } 99 | else { 100 | self.audience = VENTransactionAudiencePrivate; 101 | } 102 | 103 | 104 | // Set up VENUser actor 105 | NSDictionary *userDictionary = cleanDictionary[VENTransactionActorKey]; 106 | if ([VENUser canInitWithDictionary:userDictionary]) { 107 | VENUser *user = [[VENUser alloc] initWithDictionary:userDictionary]; 108 | self.actor = user; 109 | } 110 | 111 | 112 | // Set up VENTransactionTargets 113 | NSMutableDictionary *targetDictionary = [cleanDictionary[VENTransactionTargetKey] mutableCopy]; 114 | if (cleanDictionary[VENTransactionAmountKey]) { 115 | targetDictionary[VENTransactionAmountKey] = cleanDictionary[VENTransactionAmountKey]; 116 | } 117 | if ([VENTransactionTarget canInitWithDictionary:targetDictionary]) { 118 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithDictionary:targetDictionary]; 119 | self.target = target; 120 | } 121 | } 122 | return self; 123 | } 124 | 125 | 126 | #pragma mark - Private Instance Methods 127 | 128 | - (BOOL)isEqual:(id)object { 129 | VENTransaction *otherObject = (VENTransaction *)object; 130 | 131 | if (![otherObject.transactionID isEqualToString:self.transactionID] 132 | || otherObject.transactionType != self.transactionType) { 133 | return NO; 134 | } 135 | 136 | return YES; 137 | } 138 | 139 | @end 140 | -------------------------------------------------------------------------------- /VENCore/Models/Transactions/VENTransactionPayloadKeys.h: -------------------------------------------------------------------------------- 1 | #ifndef VENCore_VENTransactionPayloadKeys_h 2 | #define VENCore_VENTransactionPayloadKeys_h 3 | 4 | //Payment Section 5 | #define VENTransactionIDKey @"id" 6 | #define VENTransactionStatusKey @"status" 7 | #define VENTransactionNoteKey @"note" 8 | #define VENTransactionAmountKey @"amount" 9 | #define VENTransactionTypeKey @"action" 10 | #define VENTransactionAudienceKey @"audience" 11 | #define VENTransactionActorKey @"actor" 12 | #define VENTransactionTargetKey @"target" 13 | 14 | //Target Subsection 15 | #define VENTransactionTargetTypeKey @"type" 16 | #define VENTransactionTargetPhoneKey @"phone" 17 | #define VENTransactionTargetEmailKey @"email" 18 | #define VENTransactionTargetUserKey @"user" 19 | 20 | //User Subsection 21 | #define VENTransactionTargetUserIDKey @"id" 22 | 23 | 24 | //Actor Subsection 25 | #define VENTransactionActorUserID @"id" 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /VENCore/Models/Transactions/VENTransactionTarget.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @class VENUser; 4 | 5 | typedef NS_ENUM(NSUInteger, VENTargetType) { 6 | VENTargetTypeUnknown, 7 | VENTargetTypePhone, 8 | VENTargetTypeEmail, 9 | VENTargetTypeUserId 10 | }; 11 | 12 | @interface VENTransactionTarget : NSObject 13 | 14 | @property (assign, nonatomic) VENTargetType targetType; 15 | @property (copy, nonatomic) NSString *handle; // cell number, email, or Venmo user ID. 16 | @property (assign, nonatomic) NSUInteger amount; 17 | @property (copy, nonatomic) VENUser *user; 18 | 19 | /** 20 | * Determines whether the passed NSDictionary can construct a valid TransactionTarget 21 | */ 22 | + (BOOL)canInitWithDictionary:(NSDictionary *)dictionary; 23 | 24 | 25 | /** 26 | * Initializes a target with the given handle. 27 | * @param handle A phone number, email, or Venmo external user ID. 28 | * @param amount The amount in pennies. 29 | * @return A VENTransactionTarget instance 30 | */ 31 | - (instancetype)initWithHandle:(NSString *)phoneEmailOrUserID amount:(NSInteger)amount; 32 | 33 | 34 | /** 35 | * Initializes a target from a payload dictionary 36 | * @param dictionary A dictionary supplied in a payload 37 | * @return A VENTransactionTarget instance 38 | */ 39 | - (instancetype)initWithDictionary:(NSDictionary *)dictionary; 40 | 41 | 42 | /** 43 | * Returns YES if the target is valid. 44 | */ 45 | - (BOOL)isValid; 46 | 47 | 48 | /** 49 | * Sets the target's user object and sets the target's handle to the user's external ID. 50 | */ 51 | - (void)setUser:(VENUser *)user; 52 | 53 | 54 | /** 55 | * Creates a dictionary representation of the object 56 | * @return A dictionary representation of the object 57 | */ 58 | - (NSDictionary *)dictionaryRepresentation; 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /VENCore/Models/Transactions/VENTransactionTarget.m: -------------------------------------------------------------------------------- 1 | #import "VENTransactionTarget.h" 2 | #import "VENCore.h" 3 | #import "VENTransactionPayloadKeys.h" 4 | #import "NSString+VENCore.h" 5 | #import "NSDictionary+VENCore.h" 6 | #import "VENUser.h" 7 | 8 | @import CoreGraphics; 9 | 10 | @implementation VENTransactionTarget 11 | 12 | #pragma mark - Class Methods 13 | 14 | + (BOOL)canInitWithDictionary:(NSDictionary *)dictionary { 15 | 16 | NSArray *requiredKeys = @[VENTransactionAmountKey, VENTransactionTargetTypeKey]; 17 | 18 | for (NSString *key in requiredKeys) { 19 | if (!dictionary[key] || [dictionary[key] isKindOfClass:[NSNull class]]) { 20 | return NO; 21 | } 22 | } 23 | 24 | NSArray *validTargetTypes = @[VENTransactionTargetPhoneKey, VENTransactionTargetEmailKey, VENTransactionTargetUserKey]; 25 | 26 | NSString *targetType = dictionary[VENTransactionTargetTypeKey]; 27 | if (![validTargetTypes containsObject:targetType]) { 28 | return NO; 29 | } 30 | 31 | id amount = dictionary[VENTransactionAmountKey]; 32 | 33 | if ([amount isKindOfClass:[NSString class]] && ![amount intValue]) { 34 | return NO; 35 | } 36 | else if ([amount respondsToSelector:@selector(doubleValue)]) { 37 | amount = @([amount doubleValue] * 100.); 38 | } 39 | else { 40 | return NO; 41 | } 42 | 43 | return YES; 44 | } 45 | 46 | 47 | #pragma mark - Public Instance Methods 48 | 49 | - (instancetype)initWithHandle:(NSString *)phoneEmailOrUserID amount:(NSInteger)amount { 50 | if (amount < 0) { 51 | return nil; 52 | } 53 | 54 | self = [super init]; 55 | if (self) { 56 | self.handle = phoneEmailOrUserID; 57 | self.amount = amount; 58 | self.targetType = [phoneEmailOrUserID targetType]; 59 | } 60 | return self; 61 | } 62 | 63 | 64 | - (instancetype)initWithDictionary:(NSDictionary *)dictionary { 65 | 66 | self = [super init]; 67 | if (self) { 68 | 69 | if (!dictionary || ![dictionary isKindOfClass:[NSDictionary class]]) { 70 | return self; 71 | } 72 | 73 | NSDictionary *cleanDictionary = [dictionary dictionaryByCleansingResponseDictionary]; 74 | 75 | NSString *targetType = cleanDictionary[VENTransactionTargetTypeKey]; 76 | 77 | if ([targetType isEqualToString:VENTransactionTargetEmailKey]) { 78 | self.targetType = VENTargetTypeEmail; 79 | } 80 | else if ([targetType isEqualToString:VENTransactionTargetUserKey]) { 81 | self.targetType = VENTargetTypeUserId; 82 | } 83 | else if ([targetType isEqualToString:VENTransactionTargetPhoneKey]) { 84 | self.targetType = VENTargetTypePhone; 85 | } 86 | else { 87 | self.targetType = VENTargetTypeUnknown; 88 | } 89 | 90 | self.handle = cleanDictionary[targetType]; 91 | self.amount = (NSUInteger)([cleanDictionary[VENTransactionAmountKey] doubleValue] * (double)100); 92 | } 93 | return self; 94 | } 95 | 96 | 97 | - (NSDictionary *)dictionaryRepresentation { 98 | NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; 99 | if (self.handle) { 100 | //dictionary[VENTransactionTargetTypeKey] = 101 | VENTargetType targetType = [self.handle targetType]; 102 | switch (targetType) { 103 | case VENTargetTypeEmail: 104 | dictionary[VENTransactionTargetTypeKey] = VENTransactionTargetEmailKey; 105 | dictionary[VENTransactionTargetEmailKey] = self.handle; 106 | break; 107 | case VENTargetTypePhone: 108 | dictionary[VENTransactionTargetTypeKey] = VENTransactionTargetPhoneKey; 109 | dictionary[VENTransactionTargetPhoneKey] = self.handle; 110 | break; 111 | case VENTargetTypeUserId: 112 | dictionary[VENTransactionTargetTypeKey] = VENTransactionTargetUserKey; 113 | dictionary[VENTransactionTargetUserKey] = self.handle; 114 | break; 115 | default: 116 | break; 117 | } 118 | } 119 | 120 | if (self.amount) { 121 | dictionary[VENTransactionAmountKey] = @((CGFloat)self.amount/100.); 122 | } 123 | 124 | return dictionary; 125 | } 126 | 127 | 128 | - (BOOL)isValid { 129 | BOOL hasValidHandle = [self.handle isUserId] || [self.handle isUSPhone] || [self.handle isEmail]; 130 | return hasValidHandle && self.targetType != VENTargetTypeUnknown && self.amount > 0; 131 | } 132 | 133 | 134 | #pragma mark - Other Methods 135 | 136 | - (void)setUser:(VENUser *)user { 137 | _user = user; 138 | self.handle = user.externalId; 139 | self.targetType = [self.handle targetType]; 140 | } 141 | 142 | 143 | - (BOOL)isEqual:(id)object { 144 | if (![object isKindOfClass:[self class]]) { 145 | return NO; 146 | } 147 | 148 | VENTransactionTarget *otherTarget = (VENTransactionTarget *)object; 149 | 150 | if ((otherTarget.handle || self.handle) && ![otherTarget.handle isEqualToString:self.handle]) { 151 | return NO; 152 | } 153 | if (otherTarget.amount != self.amount) { 154 | return NO; 155 | } 156 | 157 | return YES; 158 | } 159 | 160 | @end 161 | -------------------------------------------------------------------------------- /VENCore/Models/Users/VENUser.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @class VENUser; 4 | 5 | typedef void(^VENUserFetchSuccessBlock)(VENUser *user); 6 | typedef void(^VENUserFetchFailureBlock)(NSError *error); 7 | typedef void(^VENFriendsFetchSuccessBlock)(NSArray *friends); 8 | typedef void(^VENFriendsFetchFailureBlock)(NSError *error); 9 | /** 10 | * @note Users are considered equal if and only if their external IDs are the same 11 | */ 12 | @interface VENUser : NSObject 13 | 14 | @property (copy, nonatomic) NSString *username; 15 | @property (copy, nonatomic) NSString *firstName; 16 | @property (copy, nonatomic) NSString *lastName; 17 | @property (copy, nonatomic) NSString *displayName; 18 | @property (copy, nonatomic) NSString *about; 19 | @property (copy, nonatomic) NSString *profileImageUrl; 20 | @property (copy, nonatomic) NSString *primaryPhone; 21 | @property (copy, nonatomic) NSString *primaryEmail; 22 | @property (copy, nonatomic) NSString *internalId; 23 | @property (copy, nonatomic) NSString *externalId; 24 | @property (strong, nonatomic) NSDate *dateJoined; 25 | 26 | 27 | /** 28 | * Initializes a user with a dictionary of User object keys. 29 | * @param The dictionary to extract keys from to create a VENUser 30 | * @return VENUser object described in dictionary 31 | * @note Calling code should call canInitWithDictionary first 32 | */ 33 | - (instancetype)initWithDictionary:(NSDictionary *)dictionary; 34 | 35 | 36 | /** 37 | * Creates a dictionary containing all keys set on the user. 38 | * This dictionary can be passed to initWithDictionary to recreate the object. 39 | 40 | * @return NSDictionary object with all fields set on the VENUser 41 | */ 42 | - (NSDictionary *)dictionaryRepresentation; 43 | 44 | 45 | /** 46 | * Determines whether the dictionary represents a valid user 47 | * @param The dictionary for evaluation of whether it can create a valid VENUser 48 | * @return BOOL indicating whether this dictionary can create a valid VENUser 49 | * @note This should be called before initWithDictionary: 50 | */ 51 | + (BOOL)canInitWithDictionary:(NSDictionary *)dictionary; 52 | 53 | 54 | /** 55 | * Asynchronously fetch a user with a given externalId 56 | * @param externalId An NSString of the desired users externalId 57 | * @param successBlock A block to be executed with the fetched user 58 | * @param failureBlock A block to be executed in case of failure / error 59 | * @note This may return from a cache or pull from the network 60 | */ 61 | + (void)fetchUserWithExternalId:(NSString *)externalId 62 | success:(VENUserFetchSuccessBlock)successBlock 63 | failure:(VENUserFetchFailureBlock)failureBlock; 64 | 65 | 66 | /** 67 | * Asynchronously fetch a user's friends with a given externalId 68 | * @param externalID An NSString of the desired user's externalId 69 | * @param successBlock A block to be executed with the fetched array of friends 70 | * @param failureBlock A block to be executed in case of failure / error 71 | */ 72 | + (void)fetchFriendsWithExternalId:(NSString *)externalId 73 | success:(VENFriendsFetchSuccessBlock)successBlock 74 | failure:(VENFriendsFetchFailureBlock)failureBlock; 75 | 76 | 77 | @end -------------------------------------------------------------------------------- /VENCore/Models/Users/VENUser.m: -------------------------------------------------------------------------------- 1 | #import "VENUser.h" 2 | #import "VENCore.h" 3 | #import "NSDictionary+VENCore.h" 4 | #import "VENUserPayloadKeys.h" 5 | 6 | @implementation VENUser 7 | 8 | - (instancetype)initWithDictionary:(NSDictionary *)dictionary { 9 | self = [super init]; 10 | 11 | if (self) { 12 | 13 | if (!dictionary || ![dictionary isKindOfClass:[NSDictionary class]]) { 14 | return self; 15 | } 16 | 17 | NSDictionary *cleanDictionary = [dictionary dictionaryByCleansingResponseDictionary]; 18 | 19 | self.username = cleanDictionary[VENUserKeyUsername]; 20 | self.firstName = cleanDictionary[VENUserKeyFirstName]; 21 | self.lastName = cleanDictionary[VENUserKeyLastName]; 22 | self.displayName = cleanDictionary[VENUserKeyDisplayName]; 23 | self.about = cleanDictionary[VENUserKeyAbout]; 24 | self.primaryPhone = cleanDictionary[VENUserKeyPhone]; 25 | self.internalId = cleanDictionary[VENUserKeyInternalId]; 26 | self.externalId = cleanDictionary[VENUserKeyExternalId]; 27 | self.dateJoined = cleanDictionary[VENUserKeyDateJoined]; 28 | self.primaryEmail = cleanDictionary[VENUserKeyEmail]; 29 | self.profileImageUrl= cleanDictionary[VENUserKeyProfileImageUrl]; 30 | } 31 | 32 | return self; 33 | } 34 | 35 | 36 | - (instancetype)copyWithZone:(NSZone *)zone { 37 | 38 | VENUser *newUser = [[[self class] alloc] init]; 39 | 40 | newUser.username = self.username; 41 | newUser.firstName = self.firstName; 42 | newUser.lastName = self.lastName; 43 | newUser.displayName = self.displayName; 44 | newUser.about = self.about; 45 | newUser.primaryPhone = self.primaryPhone; 46 | newUser.primaryEmail = self.primaryEmail; 47 | newUser.internalId = self.internalId; 48 | newUser.externalId = self.externalId; 49 | newUser.dateJoined = self.dateJoined; 50 | newUser.profileImageUrl = self.profileImageUrl; 51 | 52 | return newUser; 53 | } 54 | 55 | 56 | - (BOOL)isEqual:(id)object { 57 | 58 | if ([object class] != [self class]) { 59 | return NO; 60 | } 61 | VENUser *comparisonUser = (VENUser *)object; 62 | 63 | BOOL result = [self.externalId isEqualToString:comparisonUser.externalId]; 64 | return result; 65 | } 66 | 67 | 68 | - (NSString *)description { 69 | return [NSString stringWithFormat:@"%@ :: %@", [self class], [self dictionaryRepresentation]]; 70 | } 71 | 72 | 73 | + (BOOL)canInitWithDictionary:(NSDictionary *)dictionary { 74 | if (![dictionary isKindOfClass:[NSDictionary class]]) { 75 | return NO; 76 | } 77 | 78 | NSArray *requiredKeys = @[VENUserKeyExternalId, VENUserKeyUsername]; 79 | 80 | for (NSString *key in requiredKeys) { 81 | if (!dictionary[key] || [dictionary[key] isEqualToString:@""]) { 82 | return NO; 83 | } 84 | } 85 | return YES; 86 | } 87 | 88 | - (NSDictionary *)dictionaryRepresentation { 89 | NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; 90 | 91 | if (self.username) { 92 | dictionary[VENUserKeyUsername] = self.username; 93 | } 94 | 95 | if (self.firstName) { 96 | dictionary[VENUserKeyFirstName] = self.firstName; 97 | } 98 | 99 | if (self.lastName) { 100 | dictionary[VENUserKeyLastName] = self.lastName; 101 | } 102 | 103 | if (self.displayName) { 104 | dictionary[VENUserKeyDisplayName] = self.displayName; 105 | } 106 | 107 | if (self.about) { 108 | dictionary[VENUserKeyAbout] = self.about; 109 | } 110 | 111 | if (self.primaryPhone) { 112 | dictionary[VENUserKeyPhone] = self.primaryPhone; 113 | } 114 | 115 | if (self.primaryEmail) { 116 | dictionary[VENUserKeyEmail] = self.primaryEmail; 117 | } 118 | 119 | if (self.internalId) { 120 | dictionary[VENUserKeyInternalId] = self.internalId; 121 | } 122 | 123 | if (self.externalId) { 124 | dictionary[VENUserKeyExternalId] = self.externalId; 125 | } 126 | 127 | if (self.profileImageUrl) { 128 | dictionary[VENUserKeyProfileImageUrl] = self.profileImageUrl; 129 | } 130 | 131 | if (self.dateJoined) { 132 | dictionary[VENUserKeyDateJoined] = self.dateJoined; 133 | } 134 | 135 | return dictionary; 136 | } 137 | 138 | + (void)fetchUserWithExternalId:(NSString *)externalId 139 | success:(VENUserFetchSuccessBlock)successBlock 140 | failure:(VENUserFetchFailureBlock)failureBlock { 141 | 142 | if((![externalId isKindOfClass:[NSString class]] || ![externalId length]) && failureBlock) { 143 | NSError *error = [[NSError alloc] initWithDomain:VENErrorDomainCore 144 | code:-999 145 | userInfo:@{}]; 146 | failureBlock(error); 147 | return; 148 | } 149 | 150 | NSDictionary *parameters = @{}; 151 | 152 | [[[VENCore defaultCore] httpClient] GET:[NSString stringWithFormat:@"users/%@", externalId] 153 | parameters:parameters 154 | success:^(VENHTTPResponse *response) { 155 | 156 | NSDictionary *userPayload = [NSDictionary dictionaryWithDictionary:response.object[@"data"]]; 157 | 158 | if ([self canInitWithDictionary:userPayload] && successBlock) { 159 | VENUser *user = [[VENUser alloc] initWithDictionary:userPayload]; 160 | successBlock(user); 161 | } 162 | else if (failureBlock) { 163 | failureBlock(response.error); 164 | } 165 | } 166 | failure:^(VENHTTPResponse *response, NSError *error) { 167 | 168 | if ([response error]) { 169 | error = [response error]; 170 | } 171 | 172 | if (failureBlock) { 173 | failureBlock(error); 174 | } 175 | }]; 176 | } 177 | 178 | + (void)fetchFriendsWithExternalId:(NSString *)externalId 179 | success:(VENFriendsFetchSuccessBlock)successBlock 180 | failure:(VENFriendsFetchFailureBlock)failureBlock { 181 | if((![externalId isKindOfClass:[NSString class]] || ![externalId length]) && failureBlock) { 182 | NSError *error = [[NSError alloc] initWithDomain:VENErrorDomainCore 183 | code:-999 184 | userInfo:@{}]; 185 | failureBlock(error); 186 | return; 187 | } 188 | NSDictionary *parameters = @{@"limit": @"1000"}; 189 | [[[VENCore defaultCore] httpClient] GET:[NSString stringWithFormat:@"users/%@/friends", externalId] 190 | parameters:parameters 191 | success:^(VENHTTPResponse *response) { 192 | NSArray *friendsPayload = [NSArray arrayWithArray:response.object[@"data"]]; 193 | if(successBlock){ 194 | NSMutableArray *friendsArray = [[NSMutableArray alloc] init]; 195 | for (id object in friendsPayload) { 196 | if ([object isKindOfClass:[NSDictionary class]]) { 197 | NSDictionary *friendDictionary = (NSDictionary *)object; 198 | if ([VENUser canInitWithDictionary:friendDictionary]) { 199 | VENUser *friend = [[VENUser alloc] initWithDictionary:friendDictionary]; 200 | [friendsArray addObject:friend]; 201 | } 202 | } 203 | } 204 | successBlock(friendsArray); 205 | } 206 | else if (failureBlock){ 207 | failureBlock(response.error); 208 | } 209 | } 210 | failure:^(VENHTTPResponse *response, NSError *error){ 211 | 212 | if ([response error]) { 213 | error = [response error]; 214 | } 215 | 216 | if (failureBlock) { 217 | failureBlock(error); 218 | } 219 | 220 | }]; 221 | } 222 | 223 | @end 224 | -------------------------------------------------------------------------------- /VENCore/Models/Users/VENUserPayloadKeys.h: -------------------------------------------------------------------------------- 1 | #ifndef VENCore_VENUserPayloadKeys_h 2 | #define VENCore_VENUserPayloadKeys_h 3 | 4 | #define VENUserKeyUsername @"username" 5 | #define VENUserKeyFirstName @"first_name" 6 | #define VENUserKeyLastName @"last_name" 7 | #define VENUserKeyDisplayName @"display_name" 8 | #define VENUserKeyAbout @"about" 9 | #define VENUserKeyPhone @"phone" 10 | #define VENUserKeyProfileImageUrl @"profile_picture_url" 11 | #define VENUserKeyEmail @"email" 12 | #define VENUserKeyInternalId @"internalid" 13 | #define VENUserKeyExternalId @"id" 14 | #define VENUserKeyDateJoined @"date_joined" 15 | 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /VENCore/Networking/VENHTTP.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | extern NSString *const VENAPIPathPayments; 4 | extern NSString *const VENAPIPathUsers; 5 | 6 | @class VENHTTPResponse; 7 | 8 | @interface VENHTTP : NSObject 9 | 10 | @property (nonatomic, strong, readonly) NSURL *baseURL; 11 | 12 | - (instancetype)initWithBaseURL:(NSURL *)baseURL; 13 | 14 | - (void)setProtocolClasses:(NSArray *)protocolClasses; 15 | 16 | - (void)GET:(NSString *)path parameters:(NSDictionary *)parameters 17 | success:(void(^)(VENHTTPResponse *response))successBlock 18 | failure:(void(^)(VENHTTPResponse *response, NSError *error))failureBlock; 19 | 20 | - (void)POST:(NSString *)path parameters:(NSDictionary *)parameters 21 | success:(void(^)(VENHTTPResponse *response))successBlock 22 | failure:(void(^)(VENHTTPResponse *response, NSError *error))failureBlock; 23 | 24 | - (void)PUT:(NSString *)path parameters:(NSDictionary *)parameters 25 | success:(void(^)(VENHTTPResponse *response))successBlock 26 | failure:(void(^)(VENHTTPResponse *response, NSError *error))failureBlock; 27 | 28 | - (void)DELETE:(NSString *)path parameters:(NSDictionary *)parameters 29 | success:(void(^)(VENHTTPResponse *response))successBlock 30 | failure:(void(^)(VENHTTPResponse *response, NSError *error))failureBlock; 31 | 32 | - (void)setAccessToken:(NSString *)accessToken; 33 | 34 | - (NSDictionary *)defaultHeaders;; 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /VENCore/Networking/VENHTTP.m: -------------------------------------------------------------------------------- 1 | #import "VENHTTP.h" 2 | 3 | #import "NSError+VENCore.h" 4 | #import "VENHTTPResponse.h" 5 | #import "UIDevice+VENCore.h" 6 | #import "NSError+VENCore.h" 7 | #import "NSDictionary+VENCore.h" 8 | #import "NSArray+VENCore.h" 9 | @import CMDQueryStringSerialization; 10 | 11 | NSString *const VENAPIPathPayments = @"payments"; 12 | NSString *const VENAPIPathUsers = @"users"; 13 | 14 | @interface VENHTTP () 15 | 16 | @property (strong, nonatomic) NSString *accessToken; 17 | @property (nonatomic, strong) NSURLSession *session; 18 | @property (nonatomic, strong, readwrite) NSURL *baseURL; 19 | 20 | @end 21 | 22 | @implementation VENHTTP 23 | 24 | - (instancetype)initWithBaseURL:(NSURL *)baseURL 25 | { 26 | self = [self init]; 27 | if (self) { 28 | self.baseURL = baseURL; 29 | [self initializeSessionWithHeaders:self.defaultHeaders]; 30 | } 31 | return self; 32 | } 33 | 34 | 35 | - (void)initializeSessionWithHeaders:(NSDictionary *)headers; 36 | { 37 | void(^createSessionBlock)() = ^() { 38 | NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration]; 39 | configuration.HTTPAdditionalHeaders = self.defaultHeaders; 40 | 41 | NSOperationQueue *delegateQueue = [[NSOperationQueue alloc] init]; 42 | delegateQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount; 43 | 44 | self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:delegateQueue]; 45 | }; 46 | 47 | if (self.session) { 48 | [self.session resetWithCompletionHandler:createSessionBlock]; 49 | } 50 | else { 51 | createSessionBlock(); 52 | } 53 | } 54 | 55 | 56 | - (void)setProtocolClasses:(NSArray *)protocolClasses { 57 | NSURLSessionConfiguration *configuration = self.session.configuration; 58 | configuration.protocolClasses = protocolClasses; 59 | self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:self.session.delegateQueue]; 60 | } 61 | 62 | 63 | - (void)GET:(NSString *)path parameters:(NSDictionary *)parameters 64 | success:(void(^)(VENHTTPResponse *response))successBlock 65 | failure:(void(^)(VENHTTPResponse *response, NSError *error))failureBlock 66 | { 67 | [self sendRequestWithMethod:@"GET" path:path parameters:parameters success:successBlock failure:failureBlock]; 68 | } 69 | 70 | 71 | - (void)POST:(NSString *)path parameters:(NSDictionary *)parameters 72 | success:(void(^)(VENHTTPResponse *response))successBlock 73 | failure:(void(^)(VENHTTPResponse *response, NSError *error))failureBlock 74 | { 75 | 76 | [self sendRequestWithMethod:@"POST" path:path parameters:parameters success:successBlock failure:failureBlock]; 77 | } 78 | 79 | 80 | - (void)PUT:(NSString *)path parameters:(NSDictionary *)parameters 81 | success:(void (^)(VENHTTPResponse *))successBlock 82 | failure:(void (^)(VENHTTPResponse *, NSError *))failureBlock 83 | { 84 | [self sendRequestWithMethod:@"PUT" path:path parameters:parameters success:successBlock failure:failureBlock]; 85 | } 86 | 87 | 88 | - (void)DELETE:(NSString *)path parameters:(NSDictionary *)parameters 89 | success:(void(^)(VENHTTPResponse *response))successBlock 90 | failure:(void(^)(VENHTTPResponse *response, NSError *error))failureBlock 91 | { 92 | [self sendRequestWithMethod:@"DELETE" path:path parameters:parameters success:successBlock failure:failureBlock]; 93 | } 94 | 95 | 96 | #pragma mark - Underlying HTTP 97 | 98 | // Modified from BTHTTP 99 | - (void)sendRequestWithMethod:(NSString *)method 100 | path:(NSString *)aPath 101 | parameters:(NSDictionary *)parameters 102 | success:(void(^)(VENHTTPResponse *response))successBlock 103 | failure:(void(^)(VENHTTPResponse *response, NSError *error))failureBlock 104 | { 105 | NSURL *fullPathURL = [self.baseURL URLByAppendingPathComponent:aPath]; 106 | NSURLComponents *components = [NSURLComponents componentsWithString:fullPathURL.absoluteString]; 107 | 108 | NSMutableURLRequest *request; 109 | 110 | NSString *percentEncodedQuery = [CMDQueryStringSerialization queryStringWithDictionary:parameters]; 111 | if ([method isEqualToString:@"GET"] || [method isEqualToString:@"DELETE"]) { 112 | components.percentEncodedQuery = percentEncodedQuery; 113 | request = [NSMutableURLRequest requestWithURL:components.URL]; 114 | } else { 115 | request = [NSMutableURLRequest requestWithURL:components.URL]; 116 | NSData *body = [percentEncodedQuery dataUsingEncoding:NSUTF8StringEncoding]; 117 | [request setHTTPBody:body]; 118 | NSDictionary *headers = @{@"Content-Type": @"application/x-www-form-urlencoded; charset=utf-8"}; 119 | [request setAllHTTPHeaderFields:headers]; 120 | } 121 | // Add headers 122 | NSMutableDictionary *currentHeaders = [NSMutableDictionary dictionaryWithDictionary:request.allHTTPHeaderFields]; 123 | [currentHeaders addEntriesFromDictionary:[self headersWithAccessToken:self.accessToken]]; 124 | [request setAllHTTPHeaderFields:currentHeaders]; 125 | 126 | [request setHTTPMethod:method]; 127 | 128 | // Perform the actual request 129 | NSURLSessionTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 130 | [[self class] handleRequestCompletion:data response:response error:error success:successBlock failure:failureBlock]; 131 | }]; 132 | [task resume]; 133 | } 134 | 135 | + (void)handleRequestCompletion:(NSData *)data 136 | response:(NSURLResponse *)response 137 | error:(NSError *)error 138 | success:(void(^)(VENHTTPResponse *response))successBlock 139 | failure:(void(^)(VENHTTPResponse *response, NSError *error))failureBlock 140 | { 141 | // Handle nil or non-HTTP requests, which are an unknown type of error 142 | if (![response isKindOfClass:[NSHTTPURLResponse class]]) { 143 | NSDictionary *userInfo = error ? @{NSUnderlyingErrorKey: error} : nil; 144 | NSError *error = [NSError errorWithDomain:VENErrorDomainHTTPResponse 145 | code:VENErrorCodeHTTPResponseBadResponse 146 | userInfo:userInfo]; 147 | [self callFailureBlock:failureBlock response:nil error:error]; 148 | return; 149 | } 150 | 151 | // Attempt to parse, and return an error if parsing fails 152 | NSError *jsonParseError; 153 | id responseObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonParseError]; 154 | 155 | if (jsonParseError != nil) { 156 | [self callFailureBlock:failureBlock response:nil error:jsonParseError]; 157 | return; 158 | } 159 | 160 | if ([responseObject isKindOfClass:[NSDictionary class]]) { 161 | NSDictionary *responseDictionary = (NSDictionary *)responseObject; 162 | NSDictionary *cleansedDictionary = [responseDictionary dictionaryByCleansingResponseDictionary]; 163 | 164 | NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; 165 | VENHTTPResponse *venHTTPResponse = [[VENHTTPResponse alloc] initWithStatusCode:httpResponse.statusCode responseObject:cleansedDictionary]; 166 | if ([venHTTPResponse didError]) { 167 | [self callFailureBlock:failureBlock 168 | response:venHTTPResponse 169 | error:[venHTTPResponse error]]; 170 | } 171 | else { 172 | [self callSuccessBlock:successBlock response:venHTTPResponse]; 173 | } 174 | } 175 | else if ([responseObject isKindOfClass:[NSArray class]]) { 176 | NSArray *responseArray = (NSArray *)responseObject; 177 | NSArray *cleansedArray = [responseArray arrayByCleansingResponseArray]; 178 | NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; 179 | VENHTTPResponse *venHTTPResponse = [[VENHTTPResponse alloc] initWithStatusCode:httpResponse.statusCode responseObject:cleansedArray]; 180 | if ([venHTTPResponse didError]) { 181 | [self callFailureBlock:failureBlock 182 | response:venHTTPResponse 183 | error:[venHTTPResponse error]]; 184 | } 185 | else { 186 | [self callSuccessBlock:successBlock response:venHTTPResponse]; 187 | } 188 | } 189 | else { 190 | NSDictionary *userInfo = error ? @{NSUnderlyingErrorKey: error} : nil; 191 | NSError *error = [NSError errorWithDomain:VENErrorDomainHTTPResponse 192 | code:VENErrorCodeHTTPResponseInvalidObjectType 193 | userInfo:userInfo]; 194 | [self callFailureBlock:failureBlock response:nil error:error]; 195 | } 196 | } 197 | 198 | + (void)callSuccessBlock:(void(^)(VENHTTPResponse *response))successBlock 199 | response:(VENHTTPResponse *)response 200 | { 201 | if (!successBlock) { 202 | return; 203 | } 204 | dispatch_async(dispatch_get_main_queue(), ^{ 205 | successBlock(response); 206 | }); 207 | } 208 | 209 | + (void)callFailureBlock:(void(^)(VENHTTPResponse *response, NSError *error))failureBlock 210 | response:(VENHTTPResponse *)response 211 | error:(NSError *)error { 212 | if (!failureBlock) { 213 | return; 214 | } 215 | dispatch_async(dispatch_get_main_queue(), ^{ 216 | failureBlock(response, error); 217 | }); 218 | } 219 | 220 | - (NSDictionary *)headersWithAccessToken:(NSString *)accessToken 221 | { 222 | if (!accessToken) { 223 | return [self defaultHeaders]; 224 | } 225 | 226 | NSDictionary *cookieProperties = @{ NSHTTPCookieDomain : [self.baseURL host], 227 | NSHTTPCookiePath: @"/", 228 | NSHTTPCookieName: @"api_access_token", 229 | NSHTTPCookieValue: accessToken }; 230 | NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:cookieProperties]; 231 | // Add cookie to shared cookie storage for webview requests 232 | [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie]; 233 | NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithDictionary:[NSHTTPCookie requestHeaderFieldsWithCookies:@[cookie]]]; 234 | [headers addEntriesFromDictionary:[self defaultHeaders]]; 235 | return headers; 236 | } 237 | 238 | - (void)setAccessToken:(NSString *)accessToken 239 | { 240 | _accessToken = accessToken; 241 | NSDictionary *headers = [self headersWithAccessToken:accessToken]; 242 | [self initializeSessionWithHeaders:headers]; 243 | } 244 | 245 | 246 | - (NSDictionary *)defaultHeaders 247 | { 248 | NSMutableDictionary *defaultHeaders = [[NSMutableDictionary alloc] init]; 249 | [defaultHeaders addEntriesFromDictionary:@{@"User-Agent" : [self userAgentString], 250 | @"Accept": [self acceptString], 251 | @"Accept-Language": [self acceptLanguageString], 252 | @"Device-ID" : [[UIDevice currentDevice] VEN_deviceIDString]}]; 253 | return defaultHeaders; 254 | } 255 | 256 | 257 | - (NSString *)userAgentString 258 | { 259 | /** 260 | * Borrowed from AFNetworking 2.5.0, with modifications. 261 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43 262 | */ 263 | NSDictionary *infoDict = [NSBundle mainBundle].infoDictionary; 264 | NSString *appName = infoDict[(__bridge NSString *)kCFBundleExecutableKey] ?: infoDict[(__bridge NSString *)kCFBundleIdentifierKey] ?: @"VENCore"; 265 | NSString *appVersion = infoDict[@"CFBundleShortVersionString"] ?: infoDict[(__bridge NSString *)kCFBundleVersionKey] ?: @"0.0"; 266 | NSString *model = [[UIDevice currentDevice] VEN_platformString]; 267 | NSString *osVersion = [[UIDevice currentDevice] systemVersion]; 268 | 269 | NSString *userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", appName, appVersion, model, osVersion, [UIScreen mainScreen].scale]; 270 | 271 | if (userAgent) { 272 | if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) { 273 | NSMutableString *mutableUserAgent = [userAgent mutableCopy]; 274 | if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef) @"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) { 275 | userAgent = mutableUserAgent; 276 | } 277 | } 278 | } 279 | 280 | return userAgent; 281 | } 282 | 283 | 284 | - (NSString *)acceptString 285 | { 286 | return @"application/json"; 287 | } 288 | 289 | 290 | - (NSString *)acceptLanguageString 291 | { 292 | NSLocale *locale = [NSLocale currentLocale]; 293 | return [NSString stringWithFormat:@"%@-%@", 294 | [locale objectForKey:NSLocaleLanguageCode], 295 | [locale objectForKey:NSLocaleCountryCode]]; 296 | } 297 | 298 | @end 299 | -------------------------------------------------------------------------------- /VENCore/Networking/VENHTTPResponse.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | extern NSString *const VENErrorDomainHTTPResponse; 4 | 5 | typedef NS_ENUM(NSInteger, VEErrorCodeHTTPResponse) { 6 | VENErrorCodeHTTPResponseUnauthorizedRequest, 7 | VENErrorCodeHTTPResponseBadResponse, 8 | VENErrorCodeHTTPResponseInvalidObjectType 9 | }; 10 | 11 | @interface VENHTTPResponse : NSObject 12 | 13 | @property (nonatomic, readonly, strong) id object; 14 | @property (nonatomic, readonly, assign) NSInteger statusCode; 15 | 16 | - (instancetype)initWithStatusCode:(NSInteger)statusCode responseObject:(id)object; 17 | 18 | /** 19 | * Returns YES if the response represents an error state. 20 | */ 21 | - (BOOL)didError; 22 | 23 | 24 | /** 25 | * Returns an NSError object or nil if the response does not represent an error state. 26 | */ 27 | - (NSError *)error; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /VENCore/Networking/VENHTTPResponse.m: -------------------------------------------------------------------------------- 1 | #import "VENHTTPResponse.h" 2 | #import "NSError+VENCore.h" 3 | #import "NSDictionary+VENCore.h" 4 | 5 | NSString *const VENErrorDomainHTTPResponse = @"com.venmo.VENCore.ErrorDomain.VENHTTPResponse"; 6 | 7 | @interface VENHTTPResponse () 8 | 9 | @property (nonatomic, readwrite, strong) id object; 10 | @property (nonatomic, readwrite, assign) NSInteger statusCode; 11 | 12 | @end 13 | 14 | @implementation VENHTTPResponse 15 | 16 | - (instancetype)initWithStatusCode:(NSInteger)statusCode responseObject:(id)object { 17 | self = [self init]; 18 | if (self) { 19 | self.statusCode = statusCode; 20 | self.object = object; 21 | } 22 | return self; 23 | } 24 | 25 | 26 | - (NSString *)description { 27 | return [NSString stringWithFormat:@"", (int)self.statusCode, self.object]; 28 | } 29 | 30 | 31 | - (BOOL)didError { 32 | return self.statusCode > 299; 33 | } 34 | 35 | 36 | - (NSError *)error { 37 | if (![self didError]) { 38 | return nil; 39 | } 40 | 41 | NSDictionary *errorObject = [self.object objectOrNilForKey:@"error"]; 42 | NSString *message = [errorObject stringForKey:@"message"]; 43 | NSError *error; 44 | if (message) { 45 | NSString *codeString = [errorObject objectOrNilForKey:@"code"] ?: [errorObject[@"errors"] lastObject][@"error_code"]; 46 | NSInteger code = [codeString integerValue]; 47 | error = [NSError errorWithDomain:VENErrorDomainHTTPResponse 48 | code:code 49 | description:message 50 | recoverySuggestion:nil]; 51 | } 52 | 53 | return error; 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /VENCore/VENCore-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #ifdef __OBJC__ 8 | @import Foundation; 9 | #endif 10 | -------------------------------------------------------------------------------- /VENCore/VENCore.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | #import 4 | #import 5 | #import 6 | #import 7 | #import 8 | #import 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | 15 | extern NSString *const VENErrorDomainCore; 16 | 17 | typedef NS_ENUM(NSInteger, VENCoreErrorCode) { 18 | VENCoreErrorCodeNoDefaultCore, 19 | VENCoreErrorCodeNoAccessToken 20 | }; 21 | 22 | @interface VENCore : NSObject 23 | 24 | @property (strong, nonatomic) VENHTTP *httpClient; 25 | @property (strong, nonatomic) NSString *accessToken; 26 | 27 | 28 | /** 29 | * Sets the shared core object. 30 | * @param core The core object to share. 31 | */ 32 | + (void)setDefaultCore:(VENCore *)core; 33 | 34 | 35 | /** 36 | * Returns the shared core object. 37 | * @return A VENCore object. 38 | */ 39 | + (instancetype)defaultCore; 40 | 41 | 42 | /** 43 | * Sets the core object's access token. 44 | */ 45 | - (void)setAccessToken:(NSString *)accessToken; 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /VENCore/VENCore.m: -------------------------------------------------------------------------------- 1 | #import "VENCore.h" 2 | 3 | NSString *const VENErrorDomainCore = @"com.venmo.VENCore.ErrorDomain.VENCore"; 4 | 5 | static VENCore *sharedInstance = nil; 6 | 7 | static NSString *const VENAPIBaseURL = @"https://api.venmo.com/v1"; 8 | 9 | @implementation VENCore 10 | 11 | #pragma mark - Private 12 | 13 | - (instancetype)init { 14 | return [self initWithBaseURL:[NSURL URLWithString:VENAPIBaseURL]]; 15 | } 16 | 17 | - (instancetype)initWithBaseURL:(NSURL *)baseURL { 18 | self = [super init]; 19 | if (self) { 20 | self.httpClient = [[VENHTTP alloc] initWithBaseURL:baseURL]; 21 | } 22 | return self; 23 | } 24 | 25 | 26 | - (void)setAccessToken:(NSString *)accessToken { 27 | _accessToken = accessToken; 28 | [self.httpClient setAccessToken:accessToken]; 29 | } 30 | 31 | 32 | + (void)setDefaultCore:(VENCore *)core { 33 | sharedInstance = core; 34 | } 35 | 36 | 37 | + (instancetype)defaultCore { 38 | return sharedInstance; 39 | } 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /VENCoreIntegrationTests/PaymentSandboxSpec.m: -------------------------------------------------------------------------------- 1 | #import "VENCore.h" 2 | #import "VENCreateTransactionRequest.h" 3 | 4 | SpecBegin(PaymentSandbox) 5 | 6 | beforeAll(^{ 7 | VENCore *core = [[VENCore alloc] init]; 8 | NSURL *baseURL = [NSURL URLWithString:@"https://sandbox-api.venmo.com/v1"]; 9 | core.httpClient = [[VENHTTP alloc] initWithBaseURL:baseURL]; 10 | [core setAccessToken:[VENTestUtilities accessToken]]; 11 | [VENCore setDefaultCore:core]; 12 | }); 13 | 14 | describe(@"Settled Payment", ^{ 15 | 16 | NSUInteger amount = 10; 17 | NSString *note = @"A message to accompany the payment."; 18 | __block VENCreateTransactionRequest *transactionService; 19 | 20 | beforeEach(^{ 21 | transactionService = [[VENCreateTransactionRequest alloc] init]; 22 | }); 23 | 24 | it(@"should make a successful payment to a user id", ^AsyncBlock{ 25 | NSString *handle = @"145434160922624933"; 26 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:handle amount:amount]; 27 | transactionService.note = note; 28 | [transactionService addTransactionTarget:target]; 29 | 30 | [transactionService sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 31 | expect(sentTransactions.count).to.equal(1); 32 | VENTransaction *sentTransaction = [sentTransactions firstObject]; 33 | expect(sentTransaction.status).to.equal(VENTransactionStatusSettled); 34 | done(); 35 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 36 | failure(@"Failed to return correct response."); 37 | done(); 38 | }]; 39 | }); 40 | 41 | it(@"should make a successful payment to an email", ^AsyncBlock{ 42 | NSString *handle = @"venmo@venmo.com"; 43 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:handle amount:amount]; 44 | transactionService.note = note; 45 | [transactionService addTransactionTarget:target]; 46 | 47 | [transactionService sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 48 | expect(sentTransactions.count).to.equal(1); 49 | VENTransaction *sentTransaction = [sentTransactions firstObject]; 50 | expect(sentTransaction.status).to.equal(VENTransactionStatusSettled); 51 | done(); 52 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 53 | failure(@"Failed to return correct response."); 54 | done(); 55 | }]; 56 | }); 57 | 58 | 59 | it(@"should make a successful payment to a phone number", ^AsyncBlock{ 60 | NSString *handle = @"15555555555"; 61 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:handle amount:amount]; 62 | transactionService.note = note; 63 | [transactionService addTransactionTarget:target]; 64 | 65 | [transactionService sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 66 | expect(sentTransactions.count).to.equal(1); 67 | VENTransaction *sentTransaction = [sentTransactions firstObject]; 68 | expect(sentTransaction.status).to.equal(VENTransactionStatusSettled); 69 | done(); 70 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 71 | failure(@"Failed to return correct response."); 72 | done(); 73 | }]; 74 | }); 75 | }); 76 | 77 | describe(@"Failed Payment", ^{ 78 | 79 | NSUInteger amount = 20; 80 | NSString *note = @"A message to accompany the payment."; 81 | __block VENCreateTransactionRequest *transactionService; 82 | 83 | beforeEach(^{ 84 | transactionService = [[VENCreateTransactionRequest alloc] init]; 85 | }); 86 | 87 | it(@"should make a failed payment to an email", ^AsyncBlock{ 88 | NSString *handle = @"venmo@venmo.com"; 89 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:handle amount:amount]; 90 | transactionService.note = note; 91 | [transactionService addTransactionTarget:target]; 92 | 93 | [transactionService sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 94 | expect(sentTransactions.count).to.equal(1); 95 | VENTransaction *sentTransaction = [sentTransactions firstObject]; 96 | expect(sentTransaction.status).to.equal(VENTransactionStatusFailed); 97 | done(); 98 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 99 | failure(@"Failed to return correct response."); 100 | done(); 101 | }]; 102 | }); 103 | }); 104 | 105 | describe(@"Pending Payment", ^{ 106 | 107 | NSUInteger amount = 30; 108 | NSString *note = @"A message to accompany the payment."; 109 | __block VENCreateTransactionRequest *transactionService; 110 | 111 | beforeEach(^{ 112 | transactionService = [[VENCreateTransactionRequest alloc] init]; 113 | }); 114 | 115 | it(@"should make a pending payment to an email", ^AsyncBlock{ 116 | NSString *handle = @"foo@venmo.com"; 117 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:handle amount:amount]; 118 | transactionService.note = note; 119 | [transactionService addTransactionTarget:target]; 120 | 121 | [transactionService sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 122 | expect(sentTransactions.count).to.equal(1); 123 | VENTransaction *sentTransaction = [sentTransactions firstObject]; 124 | expect(sentTransaction.status).to.equal(VENTransactionStatusPending); 125 | done(); 126 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 127 | failure(@"Failed to return correct response."); 128 | done(); 129 | }]; 130 | 131 | }); 132 | 133 | it(@"should make a pending payment to a new phone", ^AsyncBlock{ 134 | NSString *handle = @"5555555556"; 135 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:handle amount:amount]; 136 | transactionService.note = note; 137 | [transactionService addTransactionTarget:target]; 138 | 139 | [transactionService sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 140 | expect(sentTransactions.count).to.equal(1); 141 | VENTransaction *sentTransaction = [sentTransactions firstObject]; 142 | expect(sentTransaction.status).to.equal(VENTransactionStatusPending); 143 | done(); 144 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 145 | failure(@"Failed to return correct response."); 146 | done(); 147 | }]; 148 | }); 149 | }); 150 | 151 | describe(@"Settled Charge", ^{ 152 | 153 | NSUInteger amount = 10; 154 | NSString *note = @"A message to accompany the payment."; 155 | __block VENCreateTransactionRequest *transactionRequest; 156 | 157 | beforeEach(^{ 158 | transactionRequest = [[VENCreateTransactionRequest alloc] init]; 159 | transactionRequest.transactionType = VENTransactionTypeCharge; 160 | }); 161 | 162 | it(@"should make a settled charge to a trusted email", ^AsyncBlock{ 163 | NSString *handle = @"venmo@venmo.com"; 164 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:handle amount:amount]; 165 | transactionRequest.note = note; 166 | [transactionRequest addTransactionTarget:target]; 167 | 168 | [transactionRequest sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 169 | expect(sentTransactions.count).to.equal(1); 170 | VENTransaction *sentTransaction = [sentTransactions firstObject]; 171 | expect(sentTransaction.status).to.equal(VENTransactionStatusSettled); 172 | done(); 173 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 174 | failure(@"Failed to return correct response."); 175 | done(); 176 | }]; 177 | }); 178 | }); 179 | 180 | describe(@"Pending Charge", ^{ 181 | 182 | NSUInteger amount = 20; 183 | NSString *note = @"A message to accompany the payment."; 184 | __block VENCreateTransactionRequest *transactionRequest; 185 | 186 | beforeEach(^{ 187 | transactionRequest = [[VENCreateTransactionRequest alloc] init]; 188 | transactionRequest.transactionType = VENTransactionTypeCharge; 189 | }); 190 | 191 | it(@"should make a pending charge to a non-trusted friend", ^AsyncBlock{ 192 | NSString *handle = @"venmo@venmo.com"; 193 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:handle amount:amount]; 194 | transactionRequest.note = note; 195 | [transactionRequest addTransactionTarget:target]; 196 | 197 | [transactionRequest sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 198 | expect(sentTransactions.count).to.equal(1); 199 | VENTransaction *sentTransaction = [sentTransactions firstObject]; 200 | expect(sentTransaction.status).to.equal(VENTransactionStatusPending); 201 | done(); 202 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 203 | failure(@"Failed to return correct response."); 204 | done(); 205 | }]; 206 | }); 207 | }); 208 | 209 | 210 | SpecEnd 211 | -------------------------------------------------------------------------------- /VENCoreIntegrationTests/VENCoreIntegrationTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /VENCoreIntegrationTests/VENCoreIntegrationTests-Prefix.pch: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @import XCTest; 3 | 4 | #define SPT_SHORTHAND 5 | #import 6 | 7 | #define EXP_SHORTHAND 8 | #import 9 | 10 | #import 11 | 12 | #define HC_SHORTHAND 13 | #import 14 | 15 | #import 16 | 17 | #import "VENTestUtilities.h" 18 | -------------------------------------------------------------------------------- /VENCoreIntegrationTests/VENUserIntegrationSpec.m: -------------------------------------------------------------------------------- 1 | #import "VENUser.h" 2 | #import "VENCore.h" 3 | 4 | SpecBegin(VENUserIntegration) 5 | 6 | NSString *plistPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"config" ofType:@"plist"]; 7 | NSDictionary *config = [[NSDictionary alloc] initWithContentsOfFile:plistPath]; 8 | NSString *accessToken = config[@"access_token"]; 9 | 10 | beforeAll(^{ 11 | VENCore *core = [[VENCore alloc] init]; 12 | [core setAccessToken:accessToken]; 13 | [VENCore setDefaultCore:core]; 14 | }); 15 | 16 | describe(@"Fetching a user", ^{ 17 | it(@"should retrieve a user with a correct external id", ^AsyncBlock{ 18 | NSString *externalId = @"1062502213353472181"; // (Ben) 19 | [VENUser fetchUserWithExternalId:externalId success:^(VENUser *user) { 20 | expect(user.externalId).to.equal(externalId); 21 | done(); 22 | } failure:^(NSError *error) { 23 | failure(@"Failed to return correct response."); 24 | done(); 25 | }]; 26 | }); 27 | }); 28 | 29 | SpecEnd 30 | -------------------------------------------------------------------------------- /VENCoreIntegrationTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /VENCoreUnitTests/API/errors/invalidAccessTokenError.json: -------------------------------------------------------------------------------- 1 | {"error": {"message": "You did not pass a valid OAuth access token.", "code": 261}} 2 | -------------------------------------------------------------------------------- /VENCoreUnitTests/API/errors/invalidAmountError.json: -------------------------------------------------------------------------------- 1 | {"error": {"message": "Invalid parameter : Please pass a valid amount", "code": 104}} -------------------------------------------------------------------------------- /VENCoreUnitTests/API/errors/invalidAudienceError.json: -------------------------------------------------------------------------------- 1 | {"error": {"message": "Invalid audience", "code": 3104}} -------------------------------------------------------------------------------- /VENCoreUnitTests/API/payments/paymentToEmail.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "balance": 123.00, 4 | "payment": { 5 | "id": "1322585332520059421", 6 | "status": "pending", 7 | "note": "Rock Climbing!", 8 | "amount": 4.0, 9 | "action": "pay", 10 | "date_created": "2013-12-30T19:40:57.865985", 11 | "date_completed": null, 12 | "audience": "public", 13 | "target": { 14 | "type": "email", 15 | "phone": null, 16 | "email": "nonvenmouser@gmail.com", 17 | "user": null 18 | }, 19 | "actor": { 20 | "username": "delavara", 21 | "first_name": "Cody", 22 | "last_name": "De La Vara", 23 | "display_name": "Cody De La Vara", 24 | "about": "So happy", 25 | "profile_picture_url": "https://venmopics.appspot.com/u/v3/s/6ecc7b37-5c4a-49df-b91e-3552f02dc397", 26 | "id": "1088551785594880949", 27 | "date_joined": "2013-02-10T21:58:05" 28 | }, 29 | "fee": null, 30 | "refund": null, 31 | "medium": "api", 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /VENCoreUnitTests/API/payments/paymentToUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "balance": 9.90, 4 | "payment": { 5 | "id": "1322585332520059420", 6 | "status": "settled", 7 | "note": "Rock Climbing!", 8 | "amount": 0.10, 9 | "action": "pay", 10 | "date_created": "2013-12-30T19:40:57.865985", 11 | "date_completed": "2013-12-30T19:40:57.865985", 12 | "audience": "public", 13 | "target": { 14 | "type": "user", 15 | "phone": null, 16 | "email": null, 17 | "user": { 18 | "username": "testuser", 19 | "first_name": "Test", 20 | "last_name": "User", 21 | "display_name": "Test User", 22 | "about": "Long walks on the beach, sunsets, testing", 23 | "profile_picture_url": "https://venmopics.appspot.com/u/v3/s/6ecc7b37-5c4a-49df-b91e-3552f02dc397", 24 | "id": "153136", 25 | "date_joined": "2013-02-10T21:58:05" 26 | } 27 | }, 28 | "actor": { 29 | "username": "delavara", 30 | "first_name": "Cody", 31 | "last_name": "De La Vara", 32 | "display_name": "Cody De La Vara", 33 | "about": "So happy", 34 | "profile_picture_url": "https://venmopics.appspot.com/u/v3/s/6ecc7b37-5c4a-49df-b91e-3552f02dc397", 35 | "id": "1088551785594880949", 36 | "date_joined": "2013-02-10T21:58:05" 37 | }, 38 | "fee": null, 39 | "refund": null, 40 | "medium": "api", 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /VENCoreUnitTests/API/users/fetchChrisUser.json: -------------------------------------------------------------------------------- 1 | {"data": {"username": "chrismaddern", "first_name": "Chris", "last_name": "Maddern", "display_name": "Chris Maddern", "is_friend": false, "friends_count": 239, "about": "Mobile@Venmo. Bugs may be my fault.", "email": null, "phone": null, "profile_picture_url": "https://venmopics.appspot.com/u/v8/s/43cd2f55-5a41-42bb-9634-8dccd9656673", "id": "1106387358711808333", "date_joined": "2013-03-07T12:34:11"}} 2 | -------------------------------------------------------------------------------- /VENCoreUnitTests/API/users/fetchFriends.json: -------------------------------------------------------------------------------- 1 | { 2 | "pagination":{ 3 | "next":"https://api.venmo.com/v1/users/1088551785594880949/friends?after=161568197181440668&limit=3" 4 | }, 5 | "data":[ 6 | { 7 | "username":"kortina", 8 | "about":"make a joyful sound, la da da da", 9 | "last_name":"Kortina", 10 | "display_name":"Andrew Kortina", 11 | "first_name":"Andrew", 12 | "profile_picture_url":"https://venmopics.appspot.com/u/v5/s/25f9c7a0-5d5c-4988-8737-a3278d78ae42", 13 | "id":"145434160922624167" 14 | }, 15 | { 16 | "username":"iqram", 17 | "about":"Frisbee enthusiast", 18 | "last_name":"magdon-ismail - organic", 19 | "display_name":"Iqram magdon-ismail - organic", 20 | "first_name":"Iqram", 21 | "profile_picture_url":"https://venmopics.appspot.com/u/v11/s/6c3740ad-26bd-475c-9da2-7cb3fd7591da", 22 | "id":"145436736225280235" 23 | }, 24 | { 25 | "username":"staub", 26 | "about":"phonewalletbaby", 27 | "last_name":"Staub", 28 | "display_name":"Andrew Staub", 29 | "first_name":"Andrew", 30 | "profile_picture_url":"https://venmopics.appspot.com/u/v1/s/0b4b85ab-a72b-4326-a887-5bf7a08ff445", 31 | "id":"152710129123328341" 32 | }, 33 | { 34 | "username":"azeem", 35 | "about":"No Short Bio", 36 | "last_name":"Ansar", 37 | "display_name":"Azeem Ansar", 38 | "first_name":"Azeem", 39 | "profile_picture_url":"https://venmopics.appspot.com/u/v1/s/2b797ba9-1bd5-41b8-a5a9-f133e128f234", 40 | "id":"161568197181440668" 41 | }, 42 | { 43 | "username": "great-friend", 44 | "about": "details", 45 | "display_name": "Great Friend", 46 | "first_name": "Great", 47 | "profile_picture_url": null, 48 | "id": "1106387358711808333" 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /VENCoreUnitTests/API/users/fetchInvalidFriends.json: -------------------------------------------------------------------------------- 1 | {"error": {"message": "Resource not found.", "code": 283}} 2 | -------------------------------------------------------------------------------- /VENCoreUnitTests/API/users/fetchInvalidUser.json: -------------------------------------------------------------------------------- 1 | {"error": {"message": "Resource not found.", "code": 283}} 2 | -------------------------------------------------------------------------------- /VENCoreUnitTests/Categories/NSArray+VENCoreSpec.m: -------------------------------------------------------------------------------- 1 | #import "NSArray+VENCore.h" 2 | 3 | SpecBegin(NSArrayVENCore) 4 | 5 | describe(@"stringForElement", ^{ 6 | it(@"should return the array untouched if all elements are strings", ^{ 7 | NSString *firstElement = @"elementOne"; 8 | NSString *secondElement = @"elementTwo"; 9 | NSArray *array = @[firstElement, secondElement]; 10 | [array arrayByCleansingResponseArray]; 11 | expect([array objectAtIndex:0]).to.equal(firstElement); 12 | expect([array objectAtIndex:1]).to.equal(secondElement); 13 | }); 14 | }); 15 | 16 | describe(@"cleanseResponseArray", ^{ 17 | it(@"should remove null values from an array", ^{ 18 | NSMutableArray *mutableArray = [@[[NSNull null], @"notNull", @"alsoNotNull"] mutableCopy]; 19 | [mutableArray cleanseResponseArray]; 20 | [mutableArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){ 21 | expect(obj).to.beKindOf([NSString class]); 22 | }]; 23 | }); 24 | 25 | it(@"should stringify all NSNumbers", ^{ 26 | NSMutableArray *mutableArray = [[NSMutableArray alloc] init]; 27 | [mutableArray addObject:@"thisIsAString"]; 28 | [mutableArray addObject:@"alsoAString"]; 29 | [mutableArray addObject:@3]; 30 | [mutableArray addObject:@44]; 31 | [mutableArray cleanseResponseArray]; 32 | [mutableArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){ 33 | expect(obj).to.beKindOf([NSString class]); 34 | }]; 35 | }); 36 | 37 | it(@"should not modify NSArray memebers that dont contain NSNull/NSNumber values or NSArray members", ^{ 38 | NSArray *array = @[@"Lucas", @"uses", @"Venmo"]; 39 | NSDictionary *dictionary = @{@1: @"one", 40 | @2: @"two", 41 | @3: @"3"}; 42 | NSMutableArray *mutableArray = [@[@{@"array": array, 43 | @"dictionary": dictionary}] mutableCopy]; 44 | [mutableArray cleanseResponseArray]; 45 | expect(mutableArray).to.beKindOf([NSArray class]); 46 | expect(mutableArray[0][@"array"]).to.beKindOf([NSArray class]); 47 | expect(mutableArray[0][@"dictionary"]).to.beKindOf([NSDictionary class]); 48 | }); 49 | 50 | it(@"should cleanse NSDictionary and NSArray members that contain NSNull/NSNumber values", ^{ 51 | NSArray *memberArray = @[@"Lucas", @"uses", @"Venmo"]; 52 | NSDictionary *memberDictionary = @{@1: @"one", 53 | @2: [NSNull null], 54 | @3: @3}; 55 | NSArray *array = @[memberArray, memberDictionary]; 56 | NSMutableArray *mutableArray = [array mutableCopy]; 57 | [mutableArray cleanseResponseArray]; 58 | NSDictionary *cleansedMemberDictionary = @{@1: @"one", 59 | @3: @"3"}; 60 | NSArray *cleansedArray = @[memberArray, cleansedMemberDictionary]; 61 | NSMutableArray *cleansedMutableArray = [cleansedArray mutableCopy]; 62 | expect(mutableArray).to.equal(cleansedMutableArray); 63 | }); 64 | }); 65 | 66 | SpecEnd 67 | -------------------------------------------------------------------------------- /VENCoreUnitTests/Categories/NSDictionary+VENCoreSpec.m: -------------------------------------------------------------------------------- 1 | #import "NSDictionary+VENCore.h" 2 | 3 | SpecBegin(NSDictionaryVENCore) 4 | 5 | describe(@"objectOrNilForKey:", ^{ 6 | it(@"should return the object if it exists", ^{ 7 | NSString *key = @"key"; 8 | NSObject *object = [NSObject new]; 9 | NSDictionary *d = @{key: object}; 10 | expect([d objectOrNilForKey:key]).to.equal(object); 11 | }); 12 | 13 | it(@"should return nil if the key has an NSNull object associated with it", ^{ 14 | NSString *key = @"key"; 15 | NSObject *object = [NSNull null]; 16 | NSDictionary *d = @{key: object}; 17 | expect([d objectOrNilForKey:key]).to.equal(nil); 18 | }); 19 | 20 | it(@"should return nil if there is no object", ^{ 21 | NSDictionary *d = @{@"key": @"value"}; 22 | expect([d objectOrNilForKey:@"other_key"]).to.equal(nil); 23 | }); 24 | }); 25 | 26 | 27 | describe(@"boolForKey", ^{ 28 | it(@"should return YES if the object is non-zero", ^{ 29 | NSString *key = @"key"; 30 | NSDictionary *d = @{key: @(1)}; 31 | expect([d boolForKey:key]).to.equal(YES); 32 | 33 | d = @{key: @(-1)}; 34 | expect([d boolForKey:key]).to.equal(YES); 35 | 36 | d = @{key: @(100)}; 37 | expect([d boolForKey:key]).to.equal(YES); 38 | }); 39 | 40 | it(@"should return NO if the object is 0", ^{ 41 | NSString *key = @"key"; 42 | NSDictionary *d = @{key: @(0)}; 43 | expect([d boolForKey:key]).to.equal(NO); 44 | }); 45 | 46 | it(@"should return NO if the object is NSNull", ^{ 47 | NSString *key = @"key"; 48 | NSDictionary *d = @{key: [NSNull null]}; 49 | expect([d boolForKey:key]).to.equal(NO); 50 | }); 51 | 52 | it(@"should return NO if there is no object", ^{ 53 | NSDictionary *d = @{@"key": @"value"}; 54 | expect([d boolForKey:@"other_key"]).to.equal(NO); 55 | }); 56 | }); 57 | 58 | 59 | describe(@"stringForKey", ^{ 60 | it(@"should return the object if it is a string", ^{ 61 | NSString *key = @"key"; 62 | NSString *value = @"value"; 63 | NSDictionary *d = @{key: value}; 64 | expect([d stringForKey:key]).to.equal(value); 65 | }); 66 | 67 | it(@"should return the string representation of a positive number", ^{ 68 | NSString *key = @"key"; 69 | NSNumber *value = @(123.45); 70 | NSDictionary *d = @{key: value}; 71 | expect([d stringForKey:key]).to.equal(@"123.45"); 72 | }); 73 | 74 | it(@"should return the string representation of a negative number", ^{ 75 | NSString *key = @"key"; 76 | NSNumber *value = @(-123.45); 77 | NSDictionary *d = @{key: value}; 78 | expect([d stringForKey:key]).to.equal(@"-123.45"); 79 | }); 80 | }); 81 | 82 | describe(@"cleanseResponseDictionary", ^{ 83 | it(@"should remove null values from a dictionary", ^{ 84 | NSMutableDictionary *mutableDictionary = [@{@"nullKey":[NSNull null], @"notNullKey":@"value"} mutableCopy]; 85 | [mutableDictionary cleanseResponseDictionary]; 86 | expect(mutableDictionary[@"nullKey"]).to.beNil(); 87 | expect(mutableDictionary[@"notNullKey"]).to.equal(@"value"); 88 | }); 89 | 90 | it(@"should stringify all NSNumbers", ^{ 91 | NSMutableDictionary *mutableDictionary = [@{@"numberKey": @(3), 92 | @"numberKey2": @(12830.123), 93 | @"stringKey": @"Hi there"} mutableCopy]; 94 | [mutableDictionary cleanseResponseDictionary]; 95 | 96 | [mutableDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { 97 | expect(obj).to.beKindOf([NSString class]); 98 | }]; 99 | }); 100 | 101 | it(@"should not modify NSDictionary members that dont contain NSNull/NSNumber values or NSArray members", ^{ 102 | NSArray *array = @[@"Lucas",@"uses",@"Venmo"]; 103 | NSDictionary *dictionary = @{@1: @"one", 104 | @2: @"two", 105 | @3: @"3"}; 106 | NSMutableDictionary *mutableDictionary = [@{@"array": array, 107 | @"dictionary": dictionary} mutableCopy]; 108 | 109 | [mutableDictionary cleanseResponseDictionary]; 110 | expect(mutableDictionary[@"array"]).to.beKindOf([NSArray class]); 111 | expect(mutableDictionary[@"dictionary"]).to.beKindOf([NSDictionary class]); 112 | 113 | }); 114 | 115 | it(@"should not cleanse NSDictionary members that contain NSNull values and NSNumber values", ^{ 116 | NSArray *memberArray = @[@"Lucas",@"uses",@"Venmo"]; 117 | NSDictionary *memberDictionary = @{@1: @"one", 118 | @2: [NSNull null], 119 | @3: @3}; 120 | NSMutableDictionary *mutableDictionary = [@{@"array": memberArray, 121 | @"dictionary": memberDictionary} mutableCopy]; 122 | NSDictionary *cleansedMemberDictionary = @{@1: @"one", 123 | @3: @"3"}; 124 | NSMutableDictionary *cleansedMutableDictionary = [mutableDictionary mutableCopy]; 125 | [cleansedMutableDictionary setObject:cleansedMemberDictionary 126 | forKey:@"dictionary"]; 127 | 128 | [mutableDictionary cleanseResponseDictionary]; 129 | expect(mutableDictionary).to.equal(cleansedMutableDictionary); 130 | }); 131 | 132 | }); 133 | 134 | SpecEnd -------------------------------------------------------------------------------- /VENCoreUnitTests/Categories/NSError+VENCoreSpec.m: -------------------------------------------------------------------------------- 1 | #import "NSError+VENCore.h" 2 | #import "VENHTTPResponse.h" 3 | #import "VENCore.h" 4 | 5 | SpecBegin(NSErrorVENCore) 6 | 7 | describe(@"errorWithDomain:Code:description:recoverySuggestion:", ^{ 8 | it(@"should return an NSError object with the correct domain and code", ^{ 9 | NSError *error = [NSError errorWithDomain:VENErrorDomainHTTPResponse 10 | code:123 11 | description:@"bad error" 12 | recoverySuggestion:@"deal with it"]; 13 | expect(error.code).to.equal(123); 14 | expect(error.domain).to.equal(VENErrorDomainHTTPResponse); 15 | }); 16 | 17 | it(@"should return an NSError object with the user info dictionary", ^{ 18 | NSString *domain = VENErrorDomainHTTPResponse; 19 | NSString *description = @"fatal error"; 20 | NSString *recoverySuggestion = @"deal with it"; 21 | NSError *error = [NSError errorWithDomain:domain 22 | code:123 23 | description:description 24 | recoverySuggestion:recoverySuggestion]; 25 | NSDictionary *expectedUserInfo = @{NSLocalizedDescriptionKey: description, 26 | NSLocalizedRecoverySuggestionErrorKey: recoverySuggestion}; 27 | expect(error.userInfo).to.equal(expectedUserInfo); 28 | }); 29 | }); 30 | 31 | 32 | describe(@"defaultResponseError", ^{ 33 | it(@"should return an NSError object with the correct code and user info", ^{ 34 | NSError *error = [NSError defaultResponseError]; 35 | 36 | NSString *expectedDescription = NSLocalizedString(@"Bad response", nil); 37 | expect(error.domain).to.equal(VENErrorDomainHTTPResponse); 38 | expect(error.code).to.equal(VENErrorCodeHTTPResponseBadResponse); 39 | expect(error.localizedDescription).to.equal(expectedDescription); 40 | }); 41 | }); 42 | 43 | 44 | describe(@"noDefaultCoreError", ^{ 45 | it(@"should return an NSError object with the correct code and user info", ^{ 46 | NSError *error = [NSError noDefaultCoreError]; 47 | 48 | NSString *expectedDescription = NSLocalizedString(@"No default core", nil); 49 | expect(error.domain).to.equal(VENErrorDomainCore); 50 | expect(error.code).to.equal(VENCoreErrorCodeNoDefaultCore); 51 | expect(error.localizedDescription).to.equal(expectedDescription); 52 | }); 53 | }); 54 | SpecEnd -------------------------------------------------------------------------------- /VENCoreUnitTests/Categories/NSString+VENCoreSpec.m: -------------------------------------------------------------------------------- 1 | #import "NSString+VENCore.h" 2 | 3 | SpecBegin(NSStringVENCore) 4 | 5 | describe(@"isUSPhone", ^{ 6 | it(@"should return YES if the testPhone is all numbers and 10 digits", ^{ 7 | NSString *testPhone = @"2234567890"; 8 | expect([testPhone isUSPhone]).to.equal(YES); 9 | }); 10 | 11 | it(@"should return YES if the testPhone is all numbers and 11 digits", ^{ 12 | NSString *testPhone = @"12234567890"; 13 | expect([testPhone isUSPhone]).to.equal(YES); 14 | }); 15 | 16 | it(@"should return NO if the testPhone is all numbers and > 11 digits", ^{ 17 | NSString *testPhone = @"112234567890"; 18 | expect([testPhone isUSPhone]).to.equal(NO); 19 | }); 20 | 21 | it(@"should return NO if the testPhone is all numbers and < 10 digits", ^{ 22 | NSString *testPhone = @"234567890"; 23 | expect([testPhone isUSPhone]).to.equal(NO); 24 | }); 25 | 26 | it(@"should return YES if the testPhone is valid and has hyphens and parentheses", ^{ 27 | NSString *testPhone = @"(223)456-7890"; 28 | expect([testPhone isUSPhone]).to.equal(YES); 29 | }); 30 | 31 | it(@"should return NO if the testPhone is 10 digits and has letters", ^{ 32 | NSString *testPhone = @"12222T67890"; 33 | expect([testPhone isUSPhone]).to.equal(NO); 34 | }); 35 | }); 36 | 37 | describe(@"isEmail", ^{ 38 | it(@"should return YES if the email has a handle@domain.tld", ^{ 39 | NSString *testEmail = @"test@domain.com"; 40 | expect([testEmail isEmail]).to.equal(YES); 41 | }); 42 | 43 | it(@"should return NO if the email is not handle@domain.tld", ^{ 44 | NSString *testEmail = @"test@domaincom"; 45 | expect([testEmail isEmail]).to.equal(NO); 46 | 47 | testEmail = @"testdomain.com"; 48 | expect([testEmail isEmail]).to.equal(NO); 49 | }); 50 | }); 51 | 52 | describe(@"isUserId", ^{ 53 | it(@"should return YES if the value is all numbers and not a valid phone number", ^{ 54 | NSString *userId = @"1234"; 55 | expect([userId isUserId]).to.equal(YES); 56 | }); 57 | 58 | it(@"should return NO if the value is all numbers and is a valid phone number", ^{ 59 | NSString *userId = @"1234567890"; 60 | expect([userId isUserId]).to.equal(NO); 61 | }); 62 | 63 | it(@"should return NO if the value is not all numbers", ^{ 64 | NSString *userId = @"1234T"; 65 | expect([userId isUserId]).to.equal(NO); 66 | }); 67 | }); 68 | 69 | describe(@"targetType", ^{ 70 | it(@"should return VENTargetTypePhone if the value is a phone number", ^{ 71 | NSString *testPhone = @"(223)456-7890"; 72 | expect([testPhone targetType]).to.equal(VENTargetTypePhone); 73 | }); 74 | 75 | it(@"should return VENTargetTypeUserId if the value is a userID", ^{ 76 | NSString *userId = @"1234"; 77 | expect([userId targetType]).to.equal(VENTargetTypeUserId); 78 | }); 79 | 80 | it(@"should return VENTargetTypeEmail if the value is an email address", ^{ 81 | NSString *testEmail = @"test@domain.com"; 82 | expect([testEmail targetType]).to.equal(VENTargetTypeEmail); 83 | }); 84 | 85 | it(@"should return VENTargetTypeUnknown if the value is an email address", ^{ 86 | NSString *testEmail = @"1234T"; 87 | expect([testEmail targetType]).to.equal(VENTargetTypeUnknown); 88 | }); 89 | }); 90 | 91 | describe(@"hasContent", ^{ 92 | it(@"should return YES if the value contains non-whitespace characters", ^{ 93 | NSString *string = @"*"; 94 | expect([string hasContent]).to.equal(YES); 95 | }); 96 | it(@"should return YES if the value contains non-whitespace characters", ^{ 97 | NSString *string = @" "; 98 | expect([string hasContent]).to.equal(NO); 99 | }); 100 | }); 101 | SpecEnd -------------------------------------------------------------------------------- /VENCoreUnitTests/Models/Transactions/VENCreateTransactionRequestSpec.m: -------------------------------------------------------------------------------- 1 | #import "VENTransaction.h" 2 | #import "VENTestUtilities.h" 3 | #import "VENTransactionTarget.h" 4 | #import "VENUser.h" 5 | #import "VENTransactionPayloadKeys.h" 6 | #import "VENCore.h" 7 | #import "VENHTTPResponse.h" 8 | #import "VENCreateTransactionRequest.h" 9 | 10 | @interface VENCreateTransactionRequest (Private) 11 | 12 | @property (strong, nonatomic) NSMutableOrderedSet *mutableTargets; 13 | 14 | - (NSDictionary *)dictionaryWithParametersForTarget:(VENTransactionTarget *)target; 15 | 16 | - (BOOL)containsDuplicateOfTarget:(VENTransactionTarget *)target; 17 | 18 | @end 19 | 20 | SpecBegin(VENCreateTransactionRequest) 21 | 22 | describe(@"Sending Payments With Stubbed Responses", ^{ 23 | 24 | __block NSDictionary *emailPaymentObject; 25 | __block NSDictionary *userPaymentObject; 26 | __block id mockVENHTTP; 27 | __block VENCore *core; 28 | 29 | void(^stubSuccessBlockEmail)(NSInvocation *) = ^(NSInvocation *invocation) { 30 | void(^successBlock)(VENHTTPResponse *); 31 | [invocation getArgument:&successBlock atIndex:4]; 32 | 33 | VENHTTPResponse *response = [[VENHTTPResponse alloc] initWithStatusCode:200 responseObject:@{@"data":@{@"payment":emailPaymentObject}}]; 34 | successBlock(response); 35 | }; 36 | 37 | void(^stubSuccessBlockPhone)(NSInvocation *) = ^(NSInvocation *invocation) { 38 | void(^successBlock)(VENHTTPResponse *); 39 | [invocation getArgument:&successBlock atIndex:4]; 40 | 41 | VENHTTPResponse *response = [[VENHTTPResponse alloc] initWithStatusCode:200 responseObject:@{@"data":@{@"payment":userPaymentObject}}]; 42 | successBlock(response); 43 | }; 44 | 45 | void(^stubFailureBlock)(NSInvocation *) = ^(NSInvocation *invocation) { 46 | void(^failureBlock)(VENHTTPResponse *, NSError *); 47 | [invocation getArgument:&failureBlock atIndex:5]; 48 | 49 | VENHTTPResponse *response = [[VENHTTPResponse alloc] initWithStatusCode:400 responseObject:nil]; 50 | id mockError = [OCMockObject mockForClass:[NSError class]]; 51 | failureBlock(response, mockError); 52 | }; 53 | 54 | NSDictionary *(^expectedParameters)(VENCreateTransactionRequest *transactionService, VENTransactionTarget *target) = 55 | ^(VENCreateTransactionRequest *transactionService, VENTransactionTarget *target) { 56 | NSMutableDictionary *expectedParams = [[transactionService dictionaryWithParametersForTarget:target] mutableCopy]; 57 | NSDictionary *accessTokenParams = @{@"access_token" : core.accessToken}; 58 | [expectedParams addEntriesFromDictionary:accessTokenParams]; 59 | return expectedParams; 60 | }; 61 | 62 | beforeEach(^{ 63 | NSDictionary *emailPaymentResponse = [VENTestUtilities objectFromJSONResource:@"paymentToEmail"]; 64 | emailPaymentObject = emailPaymentResponse[@"data"][@"payment"]; 65 | NSDictionary *userPaymentResponse = [VENTestUtilities objectFromJSONResource:@"paymentToUser"]; 66 | userPaymentObject = userPaymentResponse[@"data"][@"payment"]; 67 | mockVENHTTP = [OCMockObject niceMockForClass:[VENHTTP class]]; 68 | core = [[VENCore alloc] init]; 69 | core.httpClient = mockVENHTTP; 70 | [core setAccessToken:@"123"]; 71 | [VENCore setDefaultCore:core]; 72 | 73 | }); 74 | 75 | describe(@"sending a transaction with one target", ^{ 76 | it(@"should POST to the payments endpoint and call the success block when the POST succeeds", ^{ 77 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 78 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"peter@venmo.com" amount:30]; 79 | [transactionService addTransactionTarget:target]; 80 | transactionService.note = @"hi"; 81 | 82 | NSDictionary *expectedParams = expectedParameters(transactionService, target); 83 | [[[mockVENHTTP expect] andDo:stubSuccessBlockEmail] POST:@"payments" 84 | parameters:expectedParams 85 | success:OCMOCK_ANY 86 | failure:OCMOCK_ANY]; 87 | 88 | waitUntil(^(DoneCallback done) { 89 | [transactionService sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 90 | expect([sentTransactions count]).to.equal(1); 91 | done(); 92 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 93 | failure(@"Failed to return correct response."); 94 | done(); 95 | }]; 96 | }); 97 | }); 98 | 99 | it(@"should POST to the payments endpoint and call the failure block when the POST fails", ^{ 100 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 101 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"peter@venmo.com" amount:30]; 102 | [transactionService addTransactionTarget:target]; 103 | transactionService.note = @"hi"; 104 | 105 | NSDictionary *expectedParams = expectedParameters(transactionService, target); 106 | [[[mockVENHTTP expect] andDo:stubFailureBlock] POST:@"payments" 107 | parameters:expectedParams 108 | success:OCMOCK_ANY 109 | failure:OCMOCK_ANY]; 110 | 111 | waitUntil(^(DoneCallback done) { 112 | [transactionService sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 113 | failure(@"Failed to return correct response."); 114 | done(); 115 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 116 | expect([sentTransactions count]).to.equal(0); 117 | expect(response).toNot.beNil(); 118 | expect(error).toNot.beNil(); 119 | done(); 120 | }]; 121 | }); 122 | }); 123 | }); 124 | 125 | describe(@"sending a transaction with two targets", ^{ 126 | it(@"should POST twice to the payments endpoint and call the success block twice when both transactions succeed", ^{ 127 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 128 | transactionService.note = @"hi"; 129 | 130 | VENTransactionTarget *target1 = [[VENTransactionTarget alloc] initWithHandle:@"peter@venmo.com" amount:30]; 131 | [transactionService addTransactionTarget:target1]; 132 | 133 | VENTransactionTarget *target2 = [[VENTransactionTarget alloc] initWithHandle:@"ben@venmo.com" amount:420]; 134 | [transactionService addTransactionTarget:target2]; 135 | 136 | NSDictionary *expectedParameters1 = expectedParameters(transactionService, target1); 137 | NSDictionary *expectedParameters2 = expectedParameters(transactionService, target2); 138 | 139 | [[[mockVENHTTP expect] andDo:stubSuccessBlockEmail] POST:@"payments" 140 | parameters:expectedParameters1 141 | success:OCMOCK_ANY 142 | failure:OCMOCK_ANY]; 143 | [[[mockVENHTTP expect] andDo:stubSuccessBlockPhone] POST:@"payments" 144 | parameters:expectedParameters2 145 | success:OCMOCK_ANY 146 | failure:OCMOCK_ANY]; 147 | 148 | waitUntil(^(DoneCallback done) { 149 | [transactionService sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 150 | expect([sentTransactions count]).to.equal(2); 151 | done(); 152 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 153 | failure(@"Failed to return correct response."); 154 | done(); 155 | }]; 156 | }); 157 | }); 158 | 159 | it(@"should call successBlock for successful payment and failureBlock for second payment which fails", ^{ 160 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 161 | transactionService.note = @"hi"; 162 | 163 | VENTransactionTarget *target1 = [[VENTransactionTarget alloc] initWithHandle:@"peter@venmo.com" amount:30]; 164 | [transactionService addTransactionTarget:target1]; 165 | 166 | VENTransactionTarget *target2 = [[VENTransactionTarget alloc] initWithHandle:@"ben@venmo.com" amount:420]; 167 | [transactionService addTransactionTarget:target2]; 168 | 169 | NSDictionary *expectedParameters1 = expectedParameters(transactionService, target1); 170 | NSDictionary *expectedParameters2 = expectedParameters(transactionService, target2); 171 | 172 | [[[mockVENHTTP expect] andDo:stubSuccessBlockEmail] POST:@"payments" 173 | parameters:expectedParameters1 174 | success:OCMOCK_ANY 175 | failure:OCMOCK_ANY]; 176 | 177 | [[[mockVENHTTP expect] andDo:stubFailureBlock] POST:@"payments" 178 | parameters:expectedParameters2 179 | success:OCMOCK_ANY 180 | failure:OCMOCK_ANY]; 181 | 182 | waitUntil(^(DoneCallback done) { 183 | [transactionService sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 184 | failure(@"Failed to return correct response."); 185 | done(); 186 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 187 | // The failure block shouldn't be called 188 | expect([sentTransactions count]).to.equal(1); 189 | done(); 190 | }]; 191 | }); 192 | }); 193 | 194 | it(@"should not initiate second payment if the first payment fails", ^{ 195 | 196 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 197 | transactionService.note = @"hi"; 198 | 199 | VENTransactionTarget *target1 = [[VENTransactionTarget alloc] initWithHandle:@"peter@venmo.com" amount:30]; 200 | [transactionService addTransactionTarget:target1]; 201 | NSDictionary *expectedParameters1 = expectedParameters(transactionService, target1); 202 | 203 | //payment to target 2 should not be sent since the first payment fails 204 | VENTransactionTarget *target2 = [[VENTransactionTarget alloc] initWithHandle:@"das@venmo.com" amount:125]; 205 | [transactionService addTransactionTarget:target2]; 206 | 207 | [[[mockVENHTTP expect] andDo:stubFailureBlock] POST:@"payments" 208 | parameters:expectedParameters1 209 | success:OCMOCK_ANY 210 | failure:OCMOCK_ANY]; 211 | 212 | waitUntil(^(DoneCallback done) { 213 | [transactionService sendWithSuccess:^(NSArray *sentTransactions, VENHTTPResponse *response) { 214 | failure(@"Failed to return correct response."); 215 | done(); 216 | } failure:^(NSArray *sentTransactions, VENHTTPResponse *response, NSError *error) { 217 | expect([sentTransactions count]).to.equal(0); 218 | expect(response).toNot.beNil(); 219 | expect(error).toNot.beNil(); 220 | // Make sure no POSTs are made after the first one. 221 | dispatch_after(3, dispatch_get_main_queue(), ^{ 222 | done(); 223 | }); 224 | }]; 225 | }); 226 | }); 227 | }); 228 | }); 229 | 230 | 231 | describe(@"addTarget", ^{ 232 | it(@"it should add a valid target", ^{ 233 | id mockTarget = [OCMockObject niceMockForClass:[VENTransactionTarget class]]; 234 | [[[mockTarget stub] andReturnValue:@(YES)]isValid]; 235 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 236 | BOOL addedTarget = [transactionService addTransactionTarget:mockTarget]; 237 | expect(addedTarget).to.beTruthy(); 238 | expect([transactionService.targets count]).to.equal(1); 239 | }); 240 | 241 | it(@"it should not add a invalid target", ^{ 242 | id mockTarget = [OCMockObject niceMockForClass:[VENTransactionTarget class]]; 243 | [[[mockTarget stub] andReturnValue:@(NO)] isValid]; 244 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 245 | BOOL addedTarget = [transactionService addTransactionTarget:mockTarget]; 246 | expect(addedTarget).to.beFalsy(); 247 | expect([transactionService.targets count]).to.equal(0); 248 | }); 249 | 250 | it(@"it should not add duplicate targets", ^{ 251 | id mockTarget = [OCMockObject niceMockForClass:[VENTransactionTarget class]]; 252 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 253 | id mockTransaction = [OCMockObject partialMockForObject:transactionService]; 254 | [[[mockTransaction stub] andReturnValue:@(YES)] containsDuplicateOfTarget:OCMOCK_ANY]; 255 | 256 | BOOL addedTarget = [mockTransaction addTransactionTarget:mockTarget]; 257 | expect(addedTarget).to.beFalsy(); 258 | expect(((VENCreateTransactionRequest *)mockTransaction).targets.count).to.equal(0); 259 | }); 260 | }); 261 | 262 | 263 | describe(@"readyToSend", ^{ 264 | it(@"should return YES if transaction has at least one target, has a note, transactionType and status is not sent.", ^{ 265 | id object = [NSObject new]; 266 | NSOrderedSet *orderedSet = [[NSOrderedSet alloc] initWithObject:object]; 267 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 268 | id mockTransaction = [OCMockObject partialMockForObject:transactionService]; 269 | [[[mockTransaction stub] andReturn:orderedSet] mutableTargets]; 270 | ((VENCreateTransactionRequest *)mockTransaction).note = @"Here is 10 Bucks"; 271 | ((VENCreateTransactionRequest *)mockTransaction).transactionType = VENTransactionTypePay; 272 | expect([((VENCreateTransactionRequest *)mockTransaction) readyToSend]).to.equal(YES); 273 | }); 274 | 275 | it(@"should return NO if there are 0 targets", ^{ 276 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 277 | transactionService.note = @"Here is 10 Bucks"; 278 | transactionService.transactionType = VENTransactionTypePay; 279 | expect([transactionService readyToSend]).to.equal(NO); 280 | }); 281 | 282 | it(@"should return NO if transaction has no note", ^{ 283 | id object = [NSObject new]; 284 | NSOrderedSet *orderedSet = [[NSOrderedSet alloc] initWithObject:object]; 285 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 286 | id mockTransaction = [OCMockObject partialMockForObject:transactionService]; 287 | [[[mockTransaction stub] andReturn:orderedSet] mutableTargets]; 288 | ((VENCreateTransactionRequest *)mockTransaction).transactionType = VENTransactionTypePay; 289 | expect([((VENCreateTransactionRequest *)mockTransaction) readyToSend]).to.equal(NO); 290 | }); 291 | 292 | it(@"should return NO if transactionType has not been set", ^{ 293 | id object = [NSObject new]; 294 | NSOrderedSet *orderedSet = [[NSOrderedSet alloc] initWithObject:object]; 295 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 296 | id mockTransaction = [OCMockObject partialMockForObject:transactionService]; 297 | [[[mockTransaction stub] andReturn:orderedSet] mutableTargets]; 298 | ((VENCreateTransactionRequest *)mockTransaction).note = @"Here is 10 Bucks"; 299 | expect([((VENCreateTransactionRequest *)mockTransaction) readyToSend]).to.equal(NO); 300 | }); 301 | 302 | }); 303 | 304 | 305 | describe(@"dictionaryWithParametersForTarget:", ^{ 306 | it(@"should create a parameters dictionary for positive amounts", ^{ 307 | NSString *emailAddress = @"dasmer@venmo.com"; 308 | NSString *note = @"Here is two bucks"; 309 | NSString *amount = @"200"; 310 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:emailAddress amount:[amount integerValue]]; 311 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 312 | [transactionService addTransactionTarget:target]; 313 | transactionService.note = note; 314 | transactionService.transactionType = VENTransactionTypePay; 315 | transactionService.audience = VENTransactionAudienceFriends; 316 | NSDictionary *expectedPostParameters = @{@"email": emailAddress, 317 | @"note": note, 318 | @"amount" : @"2.00", 319 | @"audience" : @"friends"}; 320 | NSDictionary *postParameters = [transactionService dictionaryWithParametersForTarget:target]; 321 | expect(postParameters).to.equal(expectedPostParameters); 322 | 323 | }); 324 | 325 | it(@"should create a parameters dictionary for negative amounts", ^{ 326 | NSString *emailAddress = @"dasmer@venmo.com"; 327 | NSString *note = @"I want your two bucks"; 328 | NSString *amount = @"200"; 329 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:emailAddress amount:[amount integerValue]]; 330 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 331 | [transactionService addTransactionTarget:target]; 332 | transactionService.note = note; 333 | transactionService.transactionType = VENTransactionTypeCharge; 334 | transactionService.audience = VENTransactionAudiencePrivate; 335 | NSDictionary *expectedPostParameters = @{@"email": emailAddress, 336 | @"note": note, 337 | @"amount" : @"-2.00", 338 | @"audience" : @"private"}; 339 | NSDictionary *postParameters = [transactionService dictionaryWithParametersForTarget:target]; 340 | expect(postParameters).to.equal(expectedPostParameters); 341 | 342 | }); 343 | 344 | 345 | it(@"should return nil if target type is unknown", ^{ 346 | NSString *emailAddress = @"dasmer"; 347 | NSString *note = @"I want your two bucks"; 348 | NSString *amount = @"200"; 349 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:emailAddress amount:[amount integerValue]]; 350 | NSOrderedSet *orderedSet = [[NSOrderedSet alloc] initWithObject:target]; 351 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 352 | id mockTransaction = [OCMockObject partialMockForObject:transactionService]; 353 | [[[mockTransaction stub] andReturn:orderedSet] mutableTargets]; 354 | [transactionService addTransactionTarget:target]; 355 | ((VENCreateTransactionRequest *)mockTransaction).note = note; 356 | ((VENCreateTransactionRequest *)mockTransaction).transactionType = VENTransactionTypeCharge; 357 | ((VENCreateTransactionRequest *)mockTransaction).audience = VENTransactionAudiencePrivate; 358 | NSDictionary *postParameters = [((VENCreateTransactionRequest *)mockTransaction) dictionaryWithParametersForTarget:target]; 359 | expect(target.targetType).equal(VENTargetTypeUnknown); 360 | expect(postParameters).to.beNil(); 361 | }); 362 | 363 | 364 | it(@"should not include audience if audience is set to UserDefault", ^{ 365 | NSString *emailAddress = @"btest@example.com"; 366 | NSString *note = @"bla"; 367 | NSString *amount = @"200"; 368 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:emailAddress amount:[amount integerValue]]; 369 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 370 | [transactionService addTransactionTarget:target]; 371 | transactionService.note = note; 372 | transactionService.transactionType = VENTransactionTypePay; 373 | transactionService.audience = VENTransactionAudienceUserDefault; 374 | NSDictionary *expectedPostParameters = @{@"email": emailAddress, 375 | @"note": note, 376 | @"amount" : @"2.00"}; 377 | NSDictionary *postParameters = [transactionService dictionaryWithParametersForTarget:target]; 378 | expect(postParameters).to.equal(expectedPostParameters); 379 | }); 380 | 381 | 382 | }); 383 | 384 | describe(@"containsDuplicateOfTarget", ^{ 385 | it(@"should return YES if the transaction already has a target with the same handle", ^{ 386 | NSString *handle = @"handle"; 387 | id mockTarget1 = [OCMockObject mockForClass:[VENTransactionTarget class]]; 388 | [[[mockTarget1 stub] andReturn:handle] handle]; 389 | 390 | id mockTarget2 = [OCMockObject mockForClass:[VENTransactionTarget class]]; 391 | [[[mockTarget2 stub] andReturn:@"bla"] handle]; 392 | 393 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 394 | id mockTransaction = [OCMockObject partialMockForObject:transactionService]; 395 | NSMutableOrderedSet *targetSet = [[NSMutableOrderedSet alloc] initWithArray:@[mockTarget2, mockTarget1]]; 396 | [[[mockTransaction stub] andReturn:targetSet] targets]; 397 | 398 | id mockTargetParameter = [OCMockObject mockForClass:[VENTransactionTarget class]]; 399 | [[[mockTargetParameter stub] andReturn:handle] handle]; 400 | BOOL containsDuplicate = [mockTransaction containsDuplicateOfTarget:mockTargetParameter]; 401 | expect(containsDuplicate).to.beTruthy(); 402 | }); 403 | 404 | it(@"should return NO if the transaction doesn't have any targets with the same handle", ^{ 405 | id mockTarget1 = [OCMockObject mockForClass:[VENTransactionTarget class]]; 406 | [[[mockTarget1 stub] andReturn:@"foo"] handle]; 407 | 408 | id mockTarget2 = [OCMockObject mockForClass:[VENTransactionTarget class]]; 409 | [[[mockTarget2 stub] andReturn:@"bar"] handle]; 410 | 411 | VENCreateTransactionRequest *transactionService = [[VENCreateTransactionRequest alloc] init]; 412 | id mockTransaction = [OCMockObject partialMockForObject:transactionService]; 413 | NSMutableOrderedSet *targetSet = [[NSMutableOrderedSet alloc] initWithArray:@[mockTarget2, mockTarget1]]; 414 | [[[mockTransaction stub] andReturn:targetSet] targets]; 415 | 416 | id mockTargetParameter = [OCMockObject mockForClass:[VENTransactionTarget class]]; 417 | [[[mockTargetParameter stub] andReturn:@"baz"] handle]; 418 | BOOL containsDuplicate = [mockTransaction containsDuplicateOfTarget:mockTargetParameter]; 419 | expect(containsDuplicate).to.beFalsy(); 420 | }); 421 | }); 422 | 423 | SpecEnd 424 | -------------------------------------------------------------------------------- /VENCoreUnitTests/Models/Transactions/VENTransactionSpec.m: -------------------------------------------------------------------------------- 1 | #import "VENTransaction.h" 2 | #import "VENTestUtilities.h" 3 | #import "VENTransactionTarget.h" 4 | #import "VENUser.h" 5 | #import "VENTransactionPayloadKeys.h" 6 | #import "VENCore.h" 7 | #import "VENHTTPResponse.h" 8 | 9 | @interface VENTransaction (Private) 10 | 11 | @property (strong, nonatomic) NSMutableOrderedSet *mutableTargets; 12 | 13 | - (BOOL)containsDuplicateOfTarget:(VENTransactionTarget *)target; 14 | 15 | @end 16 | 17 | SpecBegin(VENTransaction) 18 | 19 | describe(@"Initialization", ^{ 20 | 21 | NSDictionary *paymentResponse = [VENTestUtilities objectFromJSONResource:@"paymentToEmail"]; 22 | NSDictionary *paymentObject = paymentResponse[@"data"][@"payment"]; 23 | 24 | it(@"should return YES to canInitWithDictionary for a valid transaction dictionary", ^{ 25 | NSDictionary *validTransactionDictionary = paymentObject; 26 | 27 | expect([VENTransaction canInitWithDictionary:validTransactionDictionary]).to.beTruthy(); 28 | }); 29 | 30 | it(@"should return NO to canInitWithDictionary for a transaction dictionary without an ID", ^{ 31 | NSMutableDictionary *invalidTransactionDictionary = [paymentObject mutableCopy]; 32 | [invalidTransactionDictionary removeObjectForKey:VENTransactionIDKey]; 33 | 34 | expect([VENTransaction canInitWithDictionary:invalidTransactionDictionary]).to.beFalsy(); 35 | }); 36 | 37 | it(@"should return NO to canInitWithDictionary for a transaction dictionary without any transaction targets", ^{ 38 | NSMutableDictionary *invalidTransactionDictionary = [paymentObject mutableCopy]; 39 | [invalidTransactionDictionary removeObjectForKey:VENTransactionTargetKey]; 40 | 41 | expect([VENTransaction canInitWithDictionary:invalidTransactionDictionary]).to.beFalsy(); 42 | }); 43 | 44 | it(@"should return NO to canInitWithDictionary for a transaction dictionary without a note", ^{ 45 | NSMutableDictionary *invalidTransactionDictionary = [paymentObject mutableCopy]; 46 | [invalidTransactionDictionary removeObjectForKey:VENTransactionNoteKey]; 47 | 48 | expect([VENTransaction canInitWithDictionary:invalidTransactionDictionary]).to.beFalsy(); 49 | }); 50 | 51 | it(@"should return NO to canInitWithDictionary for a transaction dictionary without an actor", ^{ 52 | NSMutableDictionary *invalidTransactionDictionary = [paymentObject mutableCopy]; 53 | [invalidTransactionDictionary removeObjectForKey:VENTransactionActorKey]; 54 | 55 | expect([VENTransaction canInitWithDictionary:invalidTransactionDictionary]).to.beFalsy(); 56 | }); 57 | 58 | it(@"should return NO to canInitWithDictionary for a transaction dictionary without an amount", ^{ 59 | NSMutableDictionary *invalidTransactionDictionary = [paymentObject mutableCopy]; 60 | [invalidTransactionDictionary removeObjectForKey:VENTransactionAmountKey]; 61 | 62 | expect([VENTransaction canInitWithDictionary:invalidTransactionDictionary]).to.beFalsy(); 63 | }); 64 | 65 | it(@"should return NO to canInitWithDictionary when the note is NSNull", ^{ 66 | NSMutableDictionary *invalidTransactionDictionary = [paymentObject mutableCopy]; 67 | invalidTransactionDictionary[VENTransactionNoteKey] = [NSNull null]; 68 | 69 | expect([VENTransaction canInitWithDictionary:invalidTransactionDictionary]).to.beFalsy(); 70 | }); 71 | 72 | it(@"should successfully initialize a transaction from a valid transaction dictionary", ^{ 73 | VENTransaction *transaction = [[VENTransaction alloc] initWithDictionary:paymentObject]; 74 | 75 | expect(transaction.transactionID).to.equal(@"1322585332520059421"); 76 | expect(transaction.note).to.equal(@"Rock Climbing!"); 77 | expect(transaction.actor.externalId).to.equal(@"1088551785594880949"); 78 | expect(transaction.transactionType).to.equal(VENTransactionTypePay); 79 | expect(transaction.status).to.equal(VENTransactionStatusPending); 80 | expect(transaction.audience).to.equal(VENTransactionAudiencePublic); 81 | expect(((VENTransactionTarget *)transaction.target).handle).to.equal(@"nonvenmouser@gmail.com"); 82 | }); 83 | }); 84 | 85 | 86 | describe(@"Equality", ^{ 87 | 88 | NSDictionary *paymentResponse = [VENTestUtilities objectFromJSONResource:@"paymentToEmail"]; 89 | NSDictionary *paymentObject = paymentResponse[@"data"][@"payment"]; 90 | 91 | it(@"should consider two identical transactions equal", ^{ 92 | VENTransaction *transaction1 = [[VENTransaction alloc] initWithDictionary:paymentObject]; 93 | VENTransaction *transaction2 = [[VENTransaction alloc] initWithDictionary:paymentObject]; 94 | 95 | expect(transaction1).to.equal(transaction2); 96 | expect(transaction2).to.equal(transaction1); 97 | }); 98 | 99 | it(@"should consider two transactions with different transaction targets different", ^{ 100 | VENTransaction *transaction = [[VENTransaction alloc] initWithDictionary:paymentObject]; 101 | 102 | NSMutableDictionary *transactionDictionary = [paymentObject mutableCopy]; 103 | [transactionDictionary removeObjectForKey:VENTransactionTargetKey]; 104 | 105 | VENTransactionTarget *newTarget = [[VENTransactionTarget alloc] initWithHandle:@"Ben Guo" amount:14]; 106 | transactionDictionary[VENTransactionTargetKey] = [newTarget dictionaryRepresentation]; 107 | VENTransaction *otherTransaction = [[VENTransaction alloc] initWithDictionary:transactionDictionary]; 108 | 109 | expect(transaction).to.equal(otherTransaction); 110 | }); 111 | 112 | it(@"should consider two identical transactions with empty targets equal", ^{ 113 | NSMutableDictionary *transactionDictionary = [paymentObject mutableCopy]; 114 | [transactionDictionary removeObjectForKey:VENTransactionTargetKey]; 115 | VENTransaction *transaction = [[VENTransaction alloc] initWithDictionary:transactionDictionary]; 116 | VENTransaction *otherTransaction = [[VENTransaction alloc] initWithDictionary:transactionDictionary]; 117 | 118 | expect(transaction).to.equal(otherTransaction); 119 | }); 120 | 121 | it(@"should consider two identical transactions but with different types inequal", ^{ 122 | NSMutableDictionary *transactionDictionary = [paymentObject mutableCopy]; 123 | NSMutableDictionary *otherTransactionDictionary = [transactionDictionary mutableCopy]; 124 | otherTransactionDictionary[VENTransactionTypeKey] = VENTransactionTypeStrings[VENTransactionTypeCharge]; 125 | VENTransaction *transaction = [[VENTransaction alloc] initWithDictionary:transactionDictionary]; 126 | VENTransaction *otherTransaction = [[VENTransaction alloc] initWithDictionary:otherTransactionDictionary]; 127 | 128 | expect(transaction).toNot.equal(otherTransaction); 129 | }); 130 | 131 | it(@"should consider transactions with different ids inequal", ^{ 132 | NSMutableDictionary *transactionDictionary = [paymentObject mutableCopy]; 133 | transactionDictionary[VENTransactionIDKey] = @"frack"; 134 | 135 | NSMutableDictionary *otherTransactionDictionary = [transactionDictionary mutableCopy]; 136 | otherTransactionDictionary[VENTransactionIDKey] = @"frick"; 137 | VENTransaction *transaction = [[VENTransaction alloc] initWithDictionary:transactionDictionary]; 138 | VENTransaction *otherTransaction = [[VENTransaction alloc] initWithDictionary:otherTransactionDictionary]; 139 | 140 | expect(transaction).toNot.equal(otherTransaction); 141 | }); 142 | 143 | it(@"should consider transactions with different statuses EQUAL", ^{ 144 | NSMutableDictionary *transactionDictionary = [paymentObject mutableCopy]; 145 | transactionDictionary[VENTransactionStatusKey] = VENTransactionStatusStrings[VENTransactionStatusUnknown]; 146 | 147 | NSMutableDictionary *otherTransactionDictionary = [transactionDictionary mutableCopy]; 148 | otherTransactionDictionary[VENTransactionStatusKey] = VENTransactionStatusStrings[VENTransactionStatusPending]; 149 | 150 | VENTransaction *transaction = [[VENTransaction alloc] initWithDictionary:transactionDictionary]; 151 | VENTransaction *otherTransaction = [[VENTransaction alloc] initWithDictionary:otherTransactionDictionary]; 152 | 153 | expect(transaction).to.equal(otherTransaction); 154 | }); 155 | 156 | }); 157 | 158 | SpecEnd -------------------------------------------------------------------------------- /VENCoreUnitTests/Models/Transactions/VENTransactionTargetSpec.m: -------------------------------------------------------------------------------- 1 | #import "VENTransactionTarget.h" 2 | #import "VENUser.h" 3 | #import "VENTestUtilities.h" 4 | #import "VENTransactionPayloadKeys.h" 5 | 6 | @interface VENUser (Internal) 7 | 8 | @property (strong, nonatomic, readwrite) NSString *externalId; 9 | 10 | @end 11 | 12 | 13 | SpecBegin(VENTransactionTarget) 14 | 15 | void(^assertTargetsAreFieldWiseEqual)(VENTransactionTarget *, VENTransactionTarget *) = ^(VENTransactionTarget *target1, VENTransactionTarget *target2) { 16 | expect(target1.amount).to.equal(target2.amount); 17 | expect(target1.handle).to.equal(target2.handle); 18 | expect(target1.targetType).to.equal(target2.targetType); 19 | }; 20 | 21 | describe(@"Initialization", ^{ 22 | 23 | NSDictionary *transactionJSON = (NSDictionary *)[VENTestUtilities objectFromJSONResource:@"paymentToEmail"]; 24 | NSMutableDictionary *targetDictionary = [NSMutableDictionary dictionaryWithDictionary:transactionJSON[@"data"][@"payment"][VENTransactionTargetKey]]; 25 | [targetDictionary setValue:@(4.12) forKey:VENTransactionAmountKey]; 26 | 27 | it(@"should return NO to canInitWithDictionary for a dictionary without an amount", ^{ 28 | NSMutableDictionary *currentPayload = [targetDictionary mutableCopy]; 29 | [currentPayload removeObjectForKey:VENTransactionAmountKey]; 30 | 31 | expect([VENTransactionTarget canInitWithDictionary:currentPayload]).to.beFalsy(); 32 | }); 33 | 34 | it(@"should return NO to canInitWithDictionary for a dictionary with an empty string amount", ^{ 35 | NSMutableDictionary *currentPayload = [targetDictionary mutableCopy]; 36 | currentPayload[VENTransactionAmountKey] = @""; 37 | 38 | expect([VENTransactionTarget canInitWithDictionary:currentPayload]).to.beFalsy(); 39 | }); 40 | 41 | it(@"should return NO to canInitWithDictionary for a dictionary without a type-field pair", ^{ 42 | NSMutableDictionary *currentPayload = [targetDictionary mutableCopy]; 43 | [currentPayload removeObjectForKey:VENTransactionTargetTypeKey]; 44 | 45 | expect([VENTransactionTarget canInitWithDictionary:currentPayload]).to.beFalsy(); 46 | }); 47 | 48 | it(@"should return YES to canInitWithDictionary for a valid target payload", ^{ 49 | expect([VENTransactionTarget canInitWithDictionary:targetDictionary]).to.beTruthy(); 50 | }); 51 | 52 | it(@"should correctly initialize a VENTransactionTarget from a valid dictionary", ^{ 53 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithDictionary:targetDictionary]; 54 | 55 | expect(target).to.beKindOf([VENTransactionTarget class]); 56 | expect(target.amount).to.equal(412); 57 | expect(target.handle).to.equal(@"nonvenmouser@gmail.com"); 58 | expect(target.targetType).to.equal(VENTargetTypeEmail); 59 | expect([target isValid]).to.equal(YES); 60 | }); 61 | }); 62 | 63 | 64 | describe(@"initWithHandle:amount:", ^{ 65 | it(@"setting the handle to an email should set the type to email", ^{ 66 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"ben@venmo.com" amount:128]; 67 | 68 | expect(target.amount).to.equal(128); 69 | expect(target.handle).to.equal(@"ben@venmo.com"); 70 | expect(target.targetType).to.equal(VENTargetTypeEmail); 71 | }); 72 | 73 | it(@"setting the handle to a phone number should set the type to phone", ^{ 74 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"2125007000" amount:10123150]; 75 | 76 | expect(target.amount).to.equal(10123150); 77 | expect(target.handle).to.equal(@"2125007000"); 78 | expect(target.targetType).to.equal(VENTargetTypePhone); 79 | }); 80 | 81 | it(@"setting the handle to a userID should set the type to userID", ^{ 82 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"1234567" amount:12]; 83 | 84 | expect(target.amount).to.equal(12); 85 | expect(target.handle).to.equal(@"1234567"); 86 | expect(target.targetType).to.equal(VENTargetTypeUserId); 87 | }); 88 | }); 89 | 90 | 91 | describe(@"isValid", ^{ 92 | it(@"should return YES if the target has a valid type and nonzero amount", ^{ 93 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"ben@venmo.com" amount:653445]; 94 | expect([target isValid]).to.equal(YES); 95 | }); 96 | 97 | it(@"should return NO if the target has type VENTargetTypeUnknown and nonzero amount", ^{ 98 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"b2124" amount:8765]; 99 | expect([target isValid]).to.equal(NO); 100 | }); 101 | 102 | it(@"should return NO if the target has type VENTargetTypeUnknown and zero amount", ^{ 103 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"b2124" amount:0]; 104 | expect([target isValid]).to.equal(NO); 105 | }); 106 | 107 | it(@"should return NO if the target has a valid type but zero amount", ^{ 108 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"4104444444" amount:0]; 109 | expect([target isValid]).to.equal(NO); 110 | }); 111 | 112 | it(@"should return NO if the target has a negative amount" , ^{ 113 | VENTransactionTarget *target =[[VENTransactionTarget alloc] initWithHandle:@"9177436332" amount:-20]; 114 | expect([target isValid]).to.equal(NO); 115 | }); 116 | }); 117 | 118 | 119 | describe(@"setUser", ^{ 120 | it(@"should set the target's user", ^{ 121 | id mockUser = [OCMockObject niceMockForClass:[VENUser class]]; 122 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"ben@venmo.com" amount:100]; 123 | target.user = mockUser; 124 | 125 | expect(target.user).to.equal(mockUser); 126 | }); 127 | 128 | it(@"should set the target's handle to the user's user id", ^{ 129 | id mockUser = [OCMockObject mockForClass:[VENUser class]]; 130 | NSString *userId = @"12345"; 131 | [[[mockUser stub] andReturn:userId] externalId]; 132 | 133 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"ben@venmo.com" amount:100]; 134 | target.user = mockUser; 135 | 136 | expect(target.handle).to.equal(userId); 137 | expect(target.targetType).to.equal(VENTargetTypeUserId); 138 | }); 139 | }); 140 | 141 | 142 | describe(@"Equality", ^{ 143 | it(@"should consider two VENTransactionTargets with the same handle and amount equal", ^{ 144 | VENTransactionTarget *target1 = [[VENTransactionTarget alloc] initWithHandle:@"peter" amount:100]; 145 | VENTransactionTarget *target2 = [[VENTransactionTarget alloc] initWithHandle:@"peter" amount:100]; 146 | 147 | expect(target1).to.equal(target2); 148 | }); 149 | 150 | it(@"should be transitive", ^{ 151 | VENTransactionTarget *target1 = [[VENTransactionTarget alloc] initWithHandle:@"peter" amount:100]; 152 | VENTransactionTarget *target2 = [[VENTransactionTarget alloc] initWithHandle:@"peter" amount:100]; 153 | 154 | expect(target1).to.equal(target2); 155 | expect(target2).to.equal(target1); 156 | }); 157 | 158 | it(@"should consider a VENTransactionTarget equal to itself", ^{ 159 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"peter" amount:1000]; 160 | 161 | expect(target).to.equal(target); 162 | }); 163 | 164 | it(@"should consider a VENTransactionTarget not to be equal to something that is not a VENTransactionTarget", ^{ 165 | NSString *notATarget = @"peter"; 166 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"Peter" amount:100]; 167 | 168 | expect(notATarget).notTo.equal(target); 169 | expect(target).notTo.equal(notATarget); 170 | }); 171 | 172 | it(@"should consider two VENTransactionTargets with the same handle but different amounts different", ^{ 173 | VENTransactionTarget *target1 = [[VENTransactionTarget alloc] initWithHandle:@"Peter" amount:50]; 174 | VENTransactionTarget *target2 = [[VENTransactionTarget alloc] initWithHandle:@"Peter" amount:100]; 175 | 176 | expect(target1).toNot.equal(target2); 177 | }); 178 | 179 | it(@"should consider two VENTransactionTargets with different handles and the same amount different", ^{ 180 | VENTransactionTarget *target1 = [[VENTransactionTarget alloc] initWithHandle:@"Peter" amount:100]; 181 | VENTransactionTarget *target2 = [[VENTransactionTarget alloc] initWithHandle:@"Piotr" amount:100]; 182 | 183 | expect(target1).toNot.equal(target2); 184 | }); 185 | 186 | it(@"should consider two empty VENTransactionTargets equal", ^{ 187 | VENTransactionTarget *target1 = [VENTransactionTarget new]; 188 | VENTransactionTarget *target2 = [VENTransactionTarget new]; 189 | 190 | expect(target2).to.equal(target1); 191 | }); 192 | 193 | it(@"should consider two VENTransactionTargets with the same handle and no amount equal", ^{ 194 | VENTransactionTarget *target1 = [VENTransactionTarget new]; 195 | VENTransactionTarget *target2 = [VENTransactionTarget new]; 196 | target1.handle = @"Peter"; 197 | target2.handle = @"Peter"; 198 | 199 | expect(target2).to.equal(target1); 200 | }); 201 | 202 | it(@"should consider two VENTransactionTargets with the same amount and no handle equal", ^{ 203 | VENTransactionTarget *target1 = [VENTransactionTarget new]; 204 | VENTransactionTarget *target2 = [VENTransactionTarget new]; 205 | target1.amount = 100; 206 | target2.amount = 100; 207 | 208 | expect(target1).to.equal(target2); 209 | }); 210 | 211 | }); 212 | 213 | describe(@"dictionaryRepresentation", ^{ 214 | it(@"should correctly represent a VENTransactionTarget in dictionary form with target type phone", ^{ 215 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"9177436332" amount:10]; 216 | NSDictionary *dictionaryRepresentation = [target dictionaryRepresentation]; 217 | NSString *targetType = dictionaryRepresentation[VENTransactionTargetTypeKey]; 218 | 219 | expect(dictionaryRepresentation[VENTransactionAmountKey]).to.equal((float)target.amount/100); 220 | expect(targetType).to.equal(@"phone"); 221 | expect(target.handle).to.equal(@"9177436332"); 222 | }); 223 | 224 | it(@"should correctly represent a VENTransactionTarget in dictionary form with target type email", ^{ 225 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"peter.zakin@gmail.com" amount:10]; 226 | NSDictionary *dictionaryRepresentation = [target dictionaryRepresentation]; 227 | NSString *targetType = dictionaryRepresentation[VENTransactionTargetTypeKey]; 228 | 229 | expect(dictionaryRepresentation[VENTransactionAmountKey]).to.equal((float)target.amount/100); 230 | expect(targetType).to.equal(@"email"); 231 | expect(target.handle).to.equal(@"peter.zakin@gmail.com"); 232 | }); 233 | 234 | it(@"should correctly represent a VENTransactionTarget in dictionary form with target type userId", ^{ 235 | VENTransactionTarget *target = [[VENTransactionTarget alloc] initWithHandle:@"1271231231239" amount:10]; 236 | NSDictionary *dictionaryRepresentation = [target dictionaryRepresentation]; 237 | NSString *targetType = dictionaryRepresentation[VENTransactionTargetTypeKey]; 238 | 239 | expect(dictionaryRepresentation[VENTransactionAmountKey]).to.equal((float)target.amount/100); 240 | expect(targetType).to.equal(VENTransactionTargetUserKey); 241 | expect(target.handle).to.equal(@"1271231231239"); 242 | }); 243 | 244 | it(@"should correctly initiate an identical VENTransactionTarget from its dictionary representation", ^{ 245 | VENTransactionTarget *target1 = [[VENTransactionTarget alloc] initWithHandle:@"1231231231233" amount:12635]; 246 | NSDictionary *dictionaryRepresentation = [target1 dictionaryRepresentation]; 247 | 248 | VENTransactionTarget *target2 = [[VENTransactionTarget alloc] initWithDictionary:dictionaryRepresentation]; 249 | 250 | expect(target1).to.equal(target2); 251 | assertTargetsAreFieldWiseEqual(target1, target2); 252 | }); 253 | }); 254 | 255 | SpecEnd -------------------------------------------------------------------------------- /VENCoreUnitTests/Models/Users/VENUserSpec.m: -------------------------------------------------------------------------------- 1 | #import "VENUser.h" 2 | #import "VENCore.h" 3 | #import "VENUserPayloadKeys.h" 4 | #import "VENTestUtilities.h" 5 | 6 | SpecBegin(VENUser) 7 | 8 | NSDictionary *validUserDictionary1 = @{VENUserKeyUsername: @"PeterIsAZakin", 9 | VENUserKeyInternalId: @"234234", 10 | VENUserKeyExternalId: @"JLHDSJFIOHh23ioHLH", 11 | VENUserKeyFirstName: @"Peter", 12 | VENUserKeyLastName: @"Maddern", 13 | VENUserKeyAbout: @"Happily married!"}; 14 | 15 | NSDictionary *validUserDictionary2 = @{VENUserKeyUsername: @"PetefadsrIsAZakin", 16 | VENUserKeyInternalId: @"234223434", 17 | VENUserKeyExternalId: @"JLHDSfadJfsdFIOHh23ioHLH", 18 | VENUserKeyFirstName: @"Pefadster", 19 | VENUserKeyLastName: @"Mafadsddern", 20 | VENUserKeyAbout: @"Happifasdly married!"}; 21 | 22 | NSDictionary *validUserDictionary3 = @{VENUserKeyUsername: @"PetefadsrIsAZakin", 23 | VENUserKeyInternalId: @"234223434", 24 | VENUserKeyExternalId: @"JLHDSJFIOHh23ioHLH", 25 | VENUserKeyFirstName: @"Pefadster", 26 | VENUserKeyLastName: @"Mafadsddern", 27 | VENUserKeyAbout: @"Happifasdly married!"}; 28 | 29 | NSDictionary *invalidUserDictionary1 = @{VENUserKeyInternalId: @"234234", 30 | VENUserKeyExternalId: @"JLHDSJFIOHh23ioHLH", 31 | VENUserKeyFirstName: @"Peter", 32 | VENUserKeyLastName: @"Maddern", 33 | VENUserKeyAbout: @"Happily married!"}; 34 | 35 | // This does not contain the external ID 36 | NSDictionary *invalidUserDictionary2 = @{VENUserKeyUsername: @"PeterIsAZakin", 37 | VENUserKeyFirstName: @"Peter", 38 | VENUserKeyLastName: @"Maddern", 39 | VENUserKeyAbout: @"Happily married!"}; 40 | 41 | 42 | void(^assertUsersAreFieldwiseEqual)(VENUser *, VENUser *) = ^(VENUser *user1, VENUser *user2) { 43 | expect(user1.username).to.equal(user2.username); 44 | expect(user1.firstName).to.equal(user2.firstName); 45 | expect(user1.lastName).to.equal(user2.lastName); 46 | expect(user1.displayName).to.equal(user2.displayName); 47 | expect(user1.about).to.equal(user2.about); 48 | expect(user1.primaryPhone).to.equal(user2.primaryPhone); 49 | expect(user1.primaryEmail).to.equal(user2.primaryEmail); 50 | expect(user1.internalId).to.equal(user2.internalId); 51 | expect(user1.externalId).to.equal(user2.externalId); 52 | expect(user1.dateJoined).to.equal(user2.dateJoined); 53 | expect(user1.profileImageUrl).to.equal(user2.profileImageUrl); 54 | }; 55 | 56 | 57 | beforeAll(^{ 58 | VENCore *core = [[VENCore alloc] init]; 59 | [VENCore setDefaultCore:core]; 60 | 61 | [[LSNocilla sharedInstance] start]; 62 | }); 63 | 64 | afterAll(^{ 65 | [[LSNocilla sharedInstance] stop]; 66 | }); 67 | 68 | afterEach(^{ 69 | [[LSNocilla sharedInstance] clearStubs]; 70 | }); 71 | 72 | 73 | describe(@"Initialization", ^{ 74 | 75 | it(@"should succesfully create an empty object from init", ^{ 76 | VENUser *usr = [[VENUser alloc] init]; 77 | expect(usr).toNot.beNil(); 78 | 79 | expect(usr.username).to.beNil(); 80 | expect(usr.firstName).to.beNil(); 81 | expect(usr.lastName).to.beNil(); 82 | expect(usr.profileImageUrl).to.beNil(); 83 | }); 84 | 85 | it(@"should return NO to canInitWithDictionary for an invalid dictionary", ^{ 86 | BOOL canInit = [VENUser canInitWithDictionary:invalidUserDictionary1]; 87 | expect(canInit).to.beFalsy(); 88 | 89 | canInit = [VENUser canInitWithDictionary:invalidUserDictionary2]; 90 | expect(canInit).to.beFalsy(); 91 | }); 92 | 93 | it(@"should return NO to a nil or empty dictionary", ^{ 94 | BOOL canInit = [VENUser canInitWithDictionary:@{}]; 95 | expect(canInit).to.beFalsy(); 96 | 97 | canInit = [VENUser canInitWithDictionary:nil]; 98 | expect(canInit).to.beFalsy(); 99 | }); 100 | 101 | it(@"should return YES to canInitWithDictionary for a valid dictionary", ^{ 102 | BOOL canInit = [VENUser canInitWithDictionary:validUserDictionary1]; 103 | expect(canInit).to.beTruthy(); 104 | }); 105 | 106 | it(@"should create a user with minimally a username and externalId for dictionaries that return YES to canInitWithDictionary",^{ 107 | VENUser *user1 = [[VENUser alloc] initWithDictionary:validUserDictionary1]; 108 | expect(user1.username).to.equal(@"PeterIsAZakin"); 109 | expect(user1.externalId).to.equal(@"JLHDSJFIOHh23ioHLH"); 110 | VENUser *user2 = [[VENUser alloc] initWithDictionary:validUserDictionary2]; 111 | expect(user2.username).to.equal(@"PetefadsrIsAZakin"); 112 | expect(user2.externalId).to.equal(@"JLHDSfadJfsdFIOHh23ioHLH"); 113 | }); 114 | 115 | }); 116 | 117 | 118 | describe(@"Copying", ^{ 119 | 120 | it(@"should create a valid copy of any empty object", ^{ 121 | VENUser *usr = [[VENUser alloc] init]; 122 | 123 | VENUser *myOtherUser = [usr copy]; 124 | 125 | expect(myOtherUser).notTo.beNil(); 126 | expect(myOtherUser).to.beKindOf([usr class]); 127 | 128 | }); 129 | 130 | it(@"should create a valid copy of a valid user", ^{ 131 | VENUser *user = [[VENUser alloc] initWithDictionary:validUserDictionary1]; 132 | VENUser *newUser = [user copy]; 133 | 134 | expect(user).to.equal(newUser); 135 | }); 136 | }); 137 | 138 | 139 | describe(@"Equality", ^{ 140 | 141 | it(@"should correctly validate two equal objects", ^{ 142 | VENUser *user1 = [[VENUser alloc] initWithDictionary:validUserDictionary1]; 143 | VENUser *user2 = [user1 copy]; 144 | 145 | expect([user1 isEqual:user2]).to.beTruthy(); 146 | 147 | VENUser *user3 = [[VENUser alloc] initWithDictionary:validUserDictionary3]; 148 | 149 | expect([user1 isEqual:user3]).to.beTruthy(); 150 | }); 151 | 152 | it(@"should not indicate that two different users are the same", ^{ 153 | VENUser *user1 = [[VENUser alloc] initWithDictionary:validUserDictionary1]; 154 | VENUser *user2 = [[VENUser alloc] initWithDictionary:validUserDictionary2]; 155 | 156 | expect([user1 isEqual:user2]).to.beFalsy(); 157 | }); 158 | 159 | it(@"should behave transitively", ^{ 160 | VENUser *user1 = [[VENUser alloc] initWithDictionary:validUserDictionary1]; 161 | VENUser *user2 = [[VENUser alloc] initWithDictionary:validUserDictionary2]; 162 | 163 | expect([user1 isEqual:user2]).to.beFalsy(); 164 | expect([user2 isEqual:user1]).to.beFalsy(); 165 | 166 | user1 = [[VENUser alloc] initWithDictionary:validUserDictionary1]; 167 | user2 = [user1 copy]; 168 | 169 | expect([user1 isEqual:user2]).to.beTruthy(); 170 | expect([user2 isEqual:user1]).to.beTruthy(); 171 | }); 172 | 173 | it(@"should follow the rule that two users are equal ONLY if their external Ids are the same", ^{ 174 | VENUser *user = [[VENUser alloc] init]; 175 | VENUser *myOtherUser = [user copy]; 176 | 177 | expect(user).toNot.equal(myOtherUser); 178 | 179 | VENUser *invalidUser = [[VENUser alloc] initWithDictionary:invalidUserDictionary2]; 180 | VENUser *copiedInvalidUser = [invalidUser copy]; 181 | 182 | expect(invalidUser).toNot.equal(copiedInvalidUser); 183 | }); 184 | 185 | }); 186 | 187 | describe(@"Dictionary Representation", ^{ 188 | it(@"should consider dictionary representations of equal users to be equal", ^{ 189 | VENUser *user1 = [[VENUser alloc] initWithDictionary:validUserDictionary1]; 190 | NSDictionary *user1Dictionary = [user1 dictionaryRepresentation]; 191 | 192 | VENUser *user2 = [[VENUser alloc] initWithDictionary:user1Dictionary]; 193 | expect([user1 isEqual:user2]).to.beTruthy(); 194 | assertUsersAreFieldwiseEqual(user1, user2); 195 | 196 | }); 197 | 198 | }); 199 | 200 | 201 | describe(@"Fetching a User", ^{ 202 | it(@"should retrieve a pre-canned Chris user and create a valid user", ^{ 203 | NSString *externalId = @"1106387358711808333"; 204 | 205 | NSString *baseURLString = [VENTestUtilities baseURLStringForCore:[VENCore defaultCore]]; 206 | NSString *urlToStub = [NSString stringWithFormat:@"%@/%@/%@?", baseURLString, VENAPIPathUsers, externalId]; 207 | 208 | [VENTestUtilities stubNetworkGET:urlToStub withStatusCode:200 andResponseFilePath:@"fetchChrisUser"]; 209 | waitUntil(^(DoneCallback done) { 210 | [VENUser fetchUserWithExternalId:externalId success:^(VENUser *user) { 211 | 212 | expect(user.externalId).to.equal(externalId); 213 | done(); 214 | } failure:^(NSError *error) { 215 | failure(@"Failed to return correct response."); 216 | done(); 217 | }]; 218 | }); 219 | 220 | }); 221 | 222 | it(@"should call failure when cannot find a user with that external Id", ^{ 223 | NSString *externalId = @"1106387358711808339"; //invalid external id 224 | 225 | NSString *baseURLString = [VENTestUtilities baseURLStringForCore:[VENCore defaultCore]]; 226 | NSString *urlToStub = [NSString stringWithFormat:@"%@/%@/%@?", baseURLString, VENAPIPathUsers, externalId]; 227 | 228 | [VENTestUtilities stubNetworkGET:urlToStub withStatusCode:400 andResponseFilePath:@"fetchInvalidUser"]; 229 | 230 | waitUntil(^(DoneCallback done) { 231 | [VENUser fetchUserWithExternalId:externalId success:^(VENUser *user) { 232 | failure(@"Failed to return correct response."); 233 | done(); 234 | } failure:^(NSError *error) { 235 | expect([error localizedDescription]).to.equal(@"Resource not found."); 236 | done(); 237 | }]; 238 | }); 239 | }); 240 | 241 | it(@"should call failure when not passed an external id", ^{ 242 | waitUntil(^(DoneCallback done) { 243 | [VENUser fetchUserWithExternalId:nil success:^(VENUser *user) { 244 | failure(@"Failed to return correct response."); 245 | done(); 246 | } failure:^(NSError *error) { 247 | expect(error).notTo.beNil(); 248 | done(); 249 | }]; 250 | }); 251 | }); 252 | 253 | it(@"should call failure when passed an empty-string external id", ^{ 254 | waitUntil(^(DoneCallback done) { 255 | [VENUser fetchUserWithExternalId:@"" success:^(VENUser *user) { 256 | failure(@"Failed to return correct response."); 257 | done(); 258 | } failure:^(NSError *error) { 259 | expect(error).notTo.beNil(); 260 | done(); 261 | }]; 262 | }); 263 | }); 264 | 265 | }); 266 | 267 | describe(@"Fetching Friends", ^{ 268 | it(@"should retrieve a pre-canned list of friends and create a valid array of friends", ^{ 269 | NSString *externalId = @"110638735871180833"; 270 | NSString *baseURLString = [VENTestUtilities baseURLStringForCore:[VENCore defaultCore]]; 271 | NSString *urlToStub = [NSString stringWithFormat:@"%@/users/%@/friends?limit=1000", baseURLString, externalId]; 272 | [VENTestUtilities stubNetworkGET:urlToStub withStatusCode:200 andResponseFilePath:@"fetchFriends"]; 273 | 274 | waitUntil(^(DoneCallback done) { 275 | [VENUser fetchFriendsWithExternalId:externalId success:^(NSArray *friendsArray) { 276 | expect([friendsArray count]).to.equal(5); 277 | expect([friendsArray[0] class]).to.equal([VENUser class]); 278 | expect([friendsArray[2] class]).to.equal([VENUser class]); 279 | done(); 280 | 281 | } failure:^(NSError *error){ 282 | failure(@"Failed to return correct response."); 283 | done(); 284 | }]; 285 | }); 286 | }); 287 | 288 | it(@"should retrieve a pre-canned list of friends and deletes the NSNull key and value from the friend", ^{ 289 | NSString *externalId = @"110638735871180833"; 290 | NSString *baseURLString = [VENTestUtilities baseURLStringForCore:[VENCore defaultCore]]; 291 | NSString *urlToStub = [NSString stringWithFormat:@"%@/users/%@/friends?limit=1000", baseURLString, externalId]; 292 | [VENTestUtilities stubNetworkGET:urlToStub withStatusCode:200 andResponseFilePath:@"fetchFriends"]; 293 | 294 | waitUntil(^(DoneCallback done) { 295 | [VENUser fetchFriendsWithExternalId:externalId success:^(NSArray *friendsArray) { 296 | for (id object in friendsArray) { 297 | if ([object isKindOfClass:[VENUser class]]) { 298 | VENUser *user = (VENUser *) object; 299 | if ([user.username isEqualToString:@"great-friend"]) { 300 | expect(user.profileImageUrl).to.beNil(); 301 | } 302 | } 303 | } 304 | done(); 305 | } failure:^(NSError *error) { 306 | failure(@"Failed to return correct response."); 307 | done(); 308 | }]; 309 | }); 310 | }); 311 | 312 | it(@"should retrieve a pre-canned list of friends and the users should be in the same order as the JSON and their values should be consistent with the JSON values", ^{ 313 | NSString *externalId = @"110638735871180833"; 314 | NSString *baseURLString = [VENTestUtilities baseURLStringForCore:[VENCore defaultCore]]; 315 | NSString *urlToStub = [NSString stringWithFormat:@"%@/users/%@/friends?limit=1000", baseURLString, externalId]; 316 | [VENTestUtilities stubNetworkGET:urlToStub withStatusCode:200 andResponseFilePath:@"fetchFriends"]; 317 | 318 | waitUntil(^(DoneCallback done) { 319 | [VENUser fetchFriendsWithExternalId:externalId success:^(NSArray *friendsArray) { 320 | if ([friendsArray[0] isKindOfClass:[VENUser class]]){ 321 | VENUser *friend = (VENUser *) friendsArray[0]; 322 | expect(friend.username).to.equal(@"kortina"); 323 | expect(friend.about).to.equal(@"make a joyful sound, la da da da"); 324 | } 325 | done(); 326 | } failure:^(NSError *error) { 327 | failure(@"Failed to return correct response."); 328 | done(); 329 | }]; 330 | }); 331 | }); 332 | 333 | it(@"should call failure when cannot find a user with that external Id", ^{ 334 | NSString *externalId = @"1106387358711808339"; //invalid external id 335 | NSString *baseURLString = [VENTestUtilities baseURLStringForCore:[VENCore defaultCore]]; 336 | NSString *urlToStub = [NSString stringWithFormat:@"%@/users/%@/friends?limit=1000", baseURLString, externalId]; 337 | [VENTestUtilities stubNetworkGET:urlToStub withStatusCode:400 andResponseFilePath:@"fetchInvalidFriends"]; 338 | 339 | waitUntil(^(DoneCallback done) { 340 | [VENUser fetchFriendsWithExternalId:externalId success:^(NSArray *friendsArray) { 341 | failure(@"Failed to return correct response."); 342 | done(); 343 | } failure:^(NSError *error) { 344 | expect([error localizedDescription]).to.equal(@"Resource not found."); 345 | done(); 346 | }]; 347 | }); 348 | }); 349 | 350 | it(@"should call failure when not passed an external id", ^{ 351 | waitUntil(^(DoneCallback done) { 352 | [VENUser fetchFriendsWithExternalId:nil success:^(NSArray *friendsArray) { 353 | failure(@"Failed to return correct response."); 354 | done(); 355 | } failure:^(NSError *error) { 356 | expect(error).notTo.beNil(); 357 | done(); 358 | }]; 359 | }); 360 | }); 361 | 362 | it(@"should call failure when passed an empty-string external id", ^{ 363 | waitUntil(^(DoneCallback done) { 364 | [VENUser fetchFriendsWithExternalId:@"" success:^(NSArray *friendsArray) { 365 | failure(@"Failed to return correct response."); 366 | done(); 367 | } failure:^(NSError *error) { 368 | expect(error).notTo.beNil(); 369 | done(); 370 | }]; 371 | }); 372 | }); 373 | }); 374 | 375 | SpecEnd 376 | -------------------------------------------------------------------------------- /VENCoreUnitTests/Networking/VENHTTPResponseSpec.m: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/venmo/VENCore/b2da9791827ba851828bedfa29bb0abcdf25656f/VENCoreUnitTests/Networking/VENHTTPResponseSpec.m -------------------------------------------------------------------------------- /VENCoreUnitTests/Networking/VENHTTPSpec.m: -------------------------------------------------------------------------------- 1 | #import "VENHTTP.h" 2 | #import "VENHTTPResponse.h" 3 | #import "VENCore.h" 4 | #import "NSString+VENCore.h" 5 | 6 | #define kVENHTTPTestProtocolScheme @"ven-http-test" 7 | #define kVENHTTPTestProtocolHost @"base.example.com" 8 | #define kVENHTTPTestProtocolBasePath @"/base/path/" 9 | #define kVENHTTPTestProtocolPort @1234 10 | 11 | @interface VENHTTPTestProtocol : NSURLProtocol 12 | @end 13 | 14 | @implementation VENHTTPTestProtocol 15 | 16 | + (BOOL)canInitWithRequest:(NSURLRequest *)request 17 | { 18 | BOOL hasCorrectScheme = [request.URL.scheme isEqualToString:kVENHTTPTestProtocolScheme]; 19 | BOOL hasCorrectHost = [request.URL.host isEqualToString:kVENHTTPTestProtocolHost]; 20 | BOOL hasCorrectPort = [request.URL.port isEqual:kVENHTTPTestProtocolPort]; 21 | BOOL hasCorrectBasePath = [request.URL.path rangeOfString:kVENHTTPTestProtocolBasePath].location != NSNotFound; 22 | 23 | return hasCorrectScheme && hasCorrectHost && hasCorrectPort && hasCorrectBasePath; 24 | } 25 | 26 | + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { 27 | return request; 28 | } 29 | 30 | - (void)startLoading { 31 | id client = self.client; 32 | 33 | NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:self.request.URL 34 | statusCode:200 35 | HTTPVersion:@"HTTP/1.1" 36 | headerFields:@{@"Content-Type": @"application/json"}]; 37 | 38 | NSData *archivedRequest = [NSKeyedArchiver archivedDataWithRootObject:self.request]; 39 | NSString *base64ArchivedRequest = [archivedRequest base64EncodedStringWithOptions:0]; 40 | 41 | NSData *requestBodyData; 42 | if (self.request.HTTPBodyStream) { 43 | NSInputStream *inputStream = self.request.HTTPBodyStream; 44 | [inputStream open]; 45 | NSMutableData *mutableBodyData = [NSMutableData data]; 46 | 47 | while ([inputStream hasBytesAvailable]) { 48 | uint8_t buffer[128]; 49 | NSUInteger bytesRead = [inputStream read:buffer maxLength:128]; 50 | [mutableBodyData appendBytes:buffer length:bytesRead]; 51 | } 52 | [inputStream close]; 53 | requestBodyData = [mutableBodyData copy]; 54 | } else { 55 | requestBodyData = self.request.HTTPBody; 56 | } 57 | 58 | NSDictionary *responseBody = @{ @"request": base64ArchivedRequest, 59 | @"requestBody": [[NSString alloc] initWithData:requestBodyData encoding:NSUTF8StringEncoding] }; 60 | 61 | [client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 62 | 63 | [client URLProtocol:self didLoadData:[NSJSONSerialization dataWithJSONObject:responseBody options:NSJSONWritingPrettyPrinted error:NULL]]; 64 | 65 | [client URLProtocolDidFinishLoading:self]; 66 | } 67 | 68 | - (void)stopLoading { 69 | } 70 | 71 | + (NSURL *)testBaseURL { 72 | NSURLComponents *components = [[NSURLComponents alloc] init]; 73 | components.scheme = kVENHTTPTestProtocolScheme; 74 | components.host = kVENHTTPTestProtocolHost; 75 | components.path = kVENHTTPTestProtocolBasePath; 76 | components.port = kVENHTTPTestProtocolPort; 77 | return components.URL; 78 | } 79 | 80 | + (NSURLRequest *)parseRequestFromTestResponse:(VENHTTPResponse *)response { 81 | return [NSKeyedUnarchiver unarchiveObjectWithData:[[NSData alloc] initWithBase64EncodedString:response.object[@"request"] options:0]]; 82 | } 83 | 84 | + (NSString *)parseRequestBodyFromTestResponse:(VENHTTPResponse *)response { 85 | return response.object[@"requestBody"]; 86 | } 87 | 88 | @end 89 | 90 | 91 | SpecBegin(VENHTTP) 92 | 93 | describe(@"performing a request", ^{ 94 | __block VENHTTP *http; 95 | 96 | beforeEach(^{ 97 | http = [[VENHTTP alloc] initWithBaseURL:[VENHTTPTestProtocol testBaseURL]]; 98 | [http setProtocolClasses:@[[VENHTTPTestProtocol class]]]; 99 | }); 100 | 101 | describe(@"base URL", ^{ 102 | it(@"sends requests using the specified URL scheme", ^{ 103 | waitUntil(^(DoneCallback done) { 104 | [http GET:@"200.json" parameters:nil success:^(VENHTTPResponse *response) { 105 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 106 | 107 | expect(httpRequest.URL.scheme).to.equal(@"ven-http-test"); 108 | done(); 109 | } failure:^(VENHTTPResponse *response, NSError *error) { 110 | failure(@"Failed to return correct response."); 111 | }]; 112 | }); 113 | }); 114 | 115 | 116 | it(@"sends requests to the host at the base URL", ^{ 117 | waitUntil(^(DoneCallback done) { 118 | [http GET:@"200.json" parameters:nil success:^(VENHTTPResponse *response) { 119 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 120 | expect(httpRequest.URL.host).to.equal(@"base.example.com"); 121 | 122 | done(); 123 | } failure:^(VENHTTPResponse *response, NSError *error) { 124 | failure(@"Failed to return correct response."); 125 | }]; 126 | }); 127 | }); 128 | 129 | it(@"appends the path to the base URL", ^{ 130 | waitUntil(^(DoneCallback done) { 131 | [http GET:@"200.json" parameters:nil success:^(VENHTTPResponse *response) { 132 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 133 | 134 | expect(httpRequest.URL.path).to.equal(@"/base/path/200.json"); 135 | done(); 136 | } failure:^(VENHTTPResponse *response, NSError *error) { 137 | failure(@"Failed to return correct response."); 138 | }]; 139 | }); 140 | }); 141 | }); 142 | 143 | describe(@"HTTP methods", ^{ 144 | it(@"sends a GET request", ^{ 145 | waitUntil(^(DoneCallback done) { 146 | [http GET:@"200.json" parameters:nil success:^(VENHTTPResponse *response) { 147 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 148 | expect(httpRequest.URL.path).to.match(@"/200.json$"); 149 | expect(httpRequest.HTTPMethod).to.equal(@"GET"); 150 | expect(httpRequest.HTTPBody).to.beNil(); 151 | done(); 152 | } failure:^(VENHTTPResponse *response, NSError *error) { 153 | failure(@"Failed to return correct response."); 154 | }]; 155 | }); 156 | }); 157 | 158 | it(@"sends a GET request with parameters", ^{ 159 | waitUntil(^(DoneCallback done) { 160 | [http GET:@"200.json" parameters:@{@"param": @"value"} success:^(VENHTTPResponse *response) { 161 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 162 | expect(httpRequest.URL.path).to.match(@"/200.json$"); 163 | expect(httpRequest.URL.query).to.equal(@"param=value"); 164 | expect(httpRequest.HTTPMethod).to.equal(@"GET"); 165 | expect(httpRequest.HTTPBody).to.beNil(); 166 | done(); 167 | } failure:^(VENHTTPResponse *response, NSError *error) { 168 | failure(@"Failed to return correct response."); 169 | }]; 170 | }); 171 | }); 172 | 173 | it(@"sends a POST request", ^{ 174 | waitUntil(^(DoneCallback done) { 175 | [http POST:@"200.json" parameters:@{@"param": @"value"} success:^(VENHTTPResponse *response) { 176 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 177 | expect(httpRequest.URL.path).to.match(@"/200.json$"); 178 | expect(httpRequest.HTTPBody).to.beNil(); 179 | expect(httpRequest.HTTPMethod).to.equal(@"POST"); 180 | expect(httpRequest.URL.query).to.beNil(); 181 | done(); 182 | } failure:^(VENHTTPResponse *response, NSError *error) { 183 | failure(@"Failed to return correct response."); 184 | }]; 185 | }); 186 | }); 187 | 188 | it(@"sends a POST request with parameters", ^{ 189 | waitUntil(^(DoneCallback done) { 190 | [http POST:@"200.json" parameters:@{@"param": @"value"} success:^(VENHTTPResponse *response) { 191 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 192 | NSString *httpRequestBody = [VENHTTPTestProtocol parseRequestBodyFromTestResponse:response]; 193 | expect(httpRequest.URL.path).to.match(@"/200.json$"); 194 | expect(httpRequestBody).to.equal(@"param=value"); 195 | expect(httpRequest.HTTPMethod).to.equal(@"POST"); 196 | expect(httpRequest.URL.query).to.beNil(); 197 | done(); 198 | } failure:^(VENHTTPResponse *response, NSError *error) { 199 | failure(@"Failed to return correct response."); 200 | }]; 201 | }); 202 | }); 203 | 204 | it(@"sends a PUT request", ^{ 205 | waitUntil(^(DoneCallback done) { 206 | [http PUT:@"200.json" parameters:@{@"param": @"value"} success:^(VENHTTPResponse *response) { 207 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 208 | expect(httpRequest.URL.path).to.match(@"200.json$"); 209 | expect(httpRequest.HTTPBody).to.beNil(); 210 | expect(httpRequest.HTTPMethod).to.equal(@"PUT"); 211 | expect(httpRequest.URL.query).to.beNil(); 212 | done(); 213 | } failure:^(VENHTTPResponse *response, NSError *error) { 214 | failure(@"Failed to return correct response."); 215 | }]; 216 | }); 217 | }); 218 | 219 | it(@"sends a PUT request with parameters", ^{ 220 | waitUntil(^(DoneCallback done) { 221 | [http PUT:@"200.json" parameters:@{@"param": @"value"} success:^(VENHTTPResponse *response) { 222 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 223 | NSString *httpRequestBody = [VENHTTPTestProtocol parseRequestBodyFromTestResponse:response]; 224 | expect(httpRequest.URL.path).to.match(@"200.json$"); 225 | expect(httpRequestBody).to.equal(@"param=value"); 226 | expect(httpRequest.HTTPMethod).to.equal(@"PUT"); 227 | expect(httpRequest.URL.query).to.beNil(); 228 | done(); 229 | } failure:^(VENHTTPResponse *response, NSError *error) { 230 | failure(@"Failed to return correct response."); 231 | }]; 232 | }); 233 | }); 234 | 235 | 236 | it(@"sends a DELETE request", ^{ 237 | waitUntil(^(DoneCallback done) { 238 | [http DELETE:@"200.json" parameters:nil success:^(VENHTTPResponse *response) { 239 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 240 | expect(httpRequest.URL.path).to.match(@"200.json$"); 241 | expect(httpRequest.HTTPBody).to.beNil(); 242 | expect(httpRequest.HTTPMethod).to.equal(@"DELETE"); 243 | expect(httpRequest.URL.query).to.equal(nil); 244 | done(); 245 | } failure:^(VENHTTPResponse *response, NSError *error) { 246 | failure(@"Failed to return correct response."); 247 | }]; 248 | }); 249 | }); 250 | 251 | it(@"sends a DELETE request with parameters", ^{ 252 | waitUntil(^(DoneCallback done) { 253 | [http DELETE:@"200.json" parameters:@{@"param": @"value"} success:^(VENHTTPResponse *response) { 254 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 255 | 256 | expect(httpRequest.URL.path).to.match(@"/200.json$"); 257 | expect(httpRequest.URL.query).to.equal(@"param=value"); 258 | expect(httpRequest.HTTPMethod).to.equal(@"DELETE"); 259 | expect(httpRequest.HTTPBody).to.beNil(); 260 | done(); 261 | } failure:^(VENHTTPResponse *response, NSError *error) { 262 | failure(@"Failed to return correct response."); 263 | }]; 264 | }); 265 | }); 266 | }); 267 | 268 | describe(@"default headers", ^{ 269 | it(@"include Accept", ^{ 270 | waitUntil(^(DoneCallback done) { 271 | [http GET:@"200.json" parameters:nil success:^(VENHTTPResponse *response) { 272 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 273 | NSDictionary *requestHeaders = httpRequest.allHTTPHeaderFields; 274 | expect(requestHeaders[@"Accept"]).to.equal(@"application/json"); 275 | done(); 276 | } failure:^(VENHTTPResponse *response, NSError *error) { 277 | failure(@"Failed to return correct response."); 278 | }]; 279 | }); 280 | }); 281 | 282 | it(@"include User-Agent", ^{ 283 | waitUntil(^(DoneCallback done) { 284 | [http GET:@"200.json" parameters:nil success:^(VENHTTPResponse *response) { 285 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 286 | NSDictionary *requestHeaders = httpRequest.allHTTPHeaderFields; 287 | expect(requestHeaders[@"User-Agent"]).to.contain(@"iOS"); 288 | done(); 289 | } failure:^(VENHTTPResponse *response, NSError *error) { 290 | failure(@"Failed to return correct response."); 291 | }]; 292 | }); 293 | }); 294 | 295 | it(@"include Accept-Language", ^{ 296 | waitUntil(^(DoneCallback done) { 297 | [http GET:@"200.json" parameters:nil success:^(VENHTTPResponse *response) { 298 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 299 | NSDictionary *requestHeaders = httpRequest.allHTTPHeaderFields; 300 | expect(requestHeaders[@"Accept-Language"]).to.equal(@"en-US"); 301 | done(); 302 | } failure:^(VENHTTPResponse *response, NSError *error) { 303 | failure(@"Failed to return correct response."); 304 | }]; 305 | }); 306 | }); 307 | }); 308 | 309 | describe(@"parameters", ^{ 310 | __block NSDictionary *parameterDictionary; 311 | 312 | beforeEach(^{ 313 | parameterDictionary = @{@"stringParam": @"a value", 314 | @"numericParam": @42, 315 | @"trueBoolParam": @YES, 316 | @"falseBoolParam": @NO 317 | }; 318 | }); 319 | 320 | describe(@"in GET requests", ^{ 321 | it(@"transmits the parameters as URL encoded query parameters", ^{ 322 | waitUntil(^(DoneCallback done) { 323 | [http GET:@"200.json" parameters:parameterDictionary success:^(VENHTTPResponse *response) { 324 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 325 | expect(httpRequest.URL.query).to.equal(@"falseBoolParam=0&numericParam=42&stringParam=a%20value&trueBoolParam=1"); 326 | done(); 327 | } failure:^(VENHTTPResponse *response, NSError *error) { 328 | failure(@"Failed to return correct response."); 329 | }]; 330 | }); 331 | }); 332 | }); 333 | 334 | describe(@"in non-GET requests", ^{ 335 | it(@"transmits the parameters as JSON", ^{ 336 | waitUntil(^(DoneCallback done) { 337 | [http POST:@"200.json" parameters:parameterDictionary success:^(VENHTTPResponse *response) { 338 | NSURLRequest *httpRequest = [VENHTTPTestProtocol parseRequestFromTestResponse:response]; 339 | NSString *httpRequestBody = [VENHTTPTestProtocol parseRequestBodyFromTestResponse:response]; 340 | 341 | expect([httpRequest valueForHTTPHeaderField:@"Content-type"]).to.equal(@"application/x-www-form-urlencoded; charset=utf-8"); 342 | expect(httpRequestBody).to.equal(@"falseBoolParam=0&numericParam=42&stringParam=a%20value&trueBoolParam=1"); 343 | 344 | done(); 345 | } failure:^(VENHTTPResponse *response, NSError *error) { 346 | failure(@"Failed to return correct response."); 347 | }]; 348 | }); 349 | }); 350 | }); 351 | }); 352 | }); 353 | 354 | SpecEnd 355 | -------------------------------------------------------------------------------- /VENCoreUnitTests/Utilities/EXPMatchers+Venmo.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | EXPMatcherInterface(regexMatch, (id pattern)) 4 | 5 | #define regexMatch regexMatch 6 | -------------------------------------------------------------------------------- /VENCoreUnitTests/Utilities/EXPMatchers+Venmo.m: -------------------------------------------------------------------------------- 1 | #import "EXPMatchers+Venmo.h" 2 | 3 | EXPMatcherImplementationBegin(regexMatch, (id pattern)) 4 | 5 | prerequisite(^BOOL{ 6 | return [actual isKindOfClass:[NSString class]] && ([pattern isKindOfClass:[NSString class]] || [pattern isKindOfClass:[NSRegularExpression class]]); 7 | }); 8 | 9 | match(^BOOL{ 10 | NSRegularExpression *regex; 11 | if ([pattern isKindOfClass:[NSRegularExpression class]]) { 12 | regex = pattern; 13 | } else { 14 | NSError *error; 15 | regex = [[NSRegularExpression alloc] initWithPattern:pattern options:0 error:&error]; 16 | NSAssert(error == nil, @"Error while compiling regular expression pattern \"%@\" (%@)", pattern, error); 17 | } 18 | 19 | return [regex numberOfMatchesInString:actual options:0 range:NSMakeRange(0, [actual length])] > 0; 20 | }); 21 | 22 | failureMessageForTo(^NSString *{ 23 | return [NSString stringWithFormat:@"expected \"%@\" to match regex pattern /%@/, but it did not", actual, pattern]; 24 | }); 25 | 26 | failureMessageForNotTo(^NSString *{ 27 | return [NSString stringWithFormat:@"expected \"%@\" not to match regex pattern /%@/, but it did", actual, pattern]; 28 | }); 29 | 30 | EXPMatcherImplementationEnd -------------------------------------------------------------------------------- /VENCoreUnitTests/Utilities/VENTestUtilities.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class VENCore; 4 | 5 | @interface VENTestUtilities : NSObject 6 | 7 | /** 8 | * Returns the Foundation object value of the given json file. 9 | */ 10 | + (id)objectFromJSONResource:(NSString *)name; 11 | 12 | 13 | /** 14 | * Returns the string value of the given json file. 15 | */ 16 | + (NSString *)stringFromJSONResource:(NSString *)name; 17 | 18 | 19 | /** 20 | * Returns the base URL string of the given core instance. 21 | */ 22 | + (NSString *)baseURLStringForCore:(VENCore *)core; 23 | 24 | /** 25 | * Stubs a GET of a URL with the contents of the file at the given path 26 | * @note Adds all default header parmeters 27 | */ 28 | + (void)stubNetworkGET:(NSString *)path 29 | withStatusCode:(NSInteger)statusCode 30 | andResponseFilePath:(NSString *)filePath; 31 | 32 | 33 | /** 34 | * Stubs a POST of a URL with the given parameters with the contents of the file at the given path 35 | * @note Adds all default header parmeters 36 | */ 37 | + (void)stubNetworkPOST:(NSString *)path 38 | forParameters:(NSDictionary *)dictionary 39 | withStatusCode:(NSInteger)statusCode 40 | andResponseFilePath:(NSString *)filePath; 41 | 42 | 43 | /** 44 | * Returns the value for the key "access_token" in the bundle's config.plist 45 | */ 46 | + (NSString *)accessToken; 47 | 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /VENCoreUnitTests/Utilities/VENTestUtilities.m: -------------------------------------------------------------------------------- 1 | #import "VENTestUtilities.h" 2 | #import "VENCore.h" 3 | 4 | @implementation VENTestUtilities 5 | 6 | + (id)objectFromJSONResource:(NSString *)name { 7 | NSString *fileString = [VENTestUtilities stringFromJSONResource:name]; 8 | return [NSJSONSerialization JSONObjectWithData:[fileString dataUsingEncoding: 9 | NSUTF8StringEncoding] options:0 error:nil]; 10 | } 11 | 12 | 13 | + (NSString *)stringFromJSONResource:(NSString *)name { 14 | NSString *filePath = [[NSBundle bundleForClass:[self class]] pathForResource:name ofType:@"json"]; 15 | NSString *fileString = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding 16 | error:NULL]; 17 | return fileString; 18 | } 19 | 20 | 21 | + (NSString *)baseURLStringForCore:(VENCore *)core { 22 | return [core.httpClient.baseURL absoluteString]; 23 | } 24 | 25 | 26 | + (void)stubNetworkGET:(NSString *)path 27 | withStatusCode:(NSInteger)statusCode 28 | andResponseFilePath:(NSString *)filePath { 29 | 30 | NSString *responseFileContents = [self stringFromJSONResource:filePath]; 31 | 32 | NSMutableDictionary *headers = [[[VENCore defaultCore].httpClient defaultHeaders] mutableCopy]; 33 | 34 | stubRequest(@"GET", path). 35 | andReturn(statusCode). 36 | withHeaders(headers). 37 | withBody(responseFileContents); 38 | } 39 | 40 | 41 | + (void)stubNetworkPOST:(NSString *)path 42 | forParameters:(NSDictionary *)dictionary 43 | withStatusCode:(NSInteger)statusCode 44 | andResponseFilePath:(NSString *)filePath { 45 | 46 | NSString *responseFileContents = [self stringFromJSONResource:filePath]; 47 | 48 | NSMutableDictionary *headers = [[[VENCore defaultCore].httpClient defaultHeaders] mutableCopy]; 49 | headers[@"Content-Type"] = @"application/json"; 50 | 51 | stubRequest(@"POST", path). 52 | andReturn(statusCode). 53 | withBody([NSKeyedArchiver archivedDataWithRootObject:dictionary]). 54 | withHeaders(headers). 55 | withBody(responseFileContents); 56 | } 57 | 58 | 59 | + (NSString *)accessToken { 60 | NSString *plistPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"config" ofType:@"plist"]; 61 | NSDictionary *config = [[NSDictionary alloc] initWithContentsOfFile:plistPath]; 62 | return config[@"access_token"]; 63 | } 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /VENCoreUnitTests/VENCoreSpec.m: -------------------------------------------------------------------------------- 1 | #import "VENCore.h" 2 | #import "VENHTTP.h" 3 | #import "VENUser.h" 4 | 5 | NSString *accessToken; 6 | NSDictionary *responseDictionary; 7 | NSString *responseString; 8 | NSURL *baseURL; 9 | VENCore *core; 10 | NSString *path; 11 | 12 | SpecBegin(VENCore) 13 | 14 | [Expecta setAsynchronousTestTimeout:1]; 15 | 16 | describe(@"Shared Instances of VENCore should persist", ^{ 17 | 18 | it(@"should set a 'defaultCore' and be retrievable", ^{ 19 | VENCore *newCore = [[VENCore alloc] init]; 20 | [VENCore setDefaultCore:newCore]; 21 | 22 | expect([VENCore defaultCore]).to.equal(newCore); 23 | }); 24 | 25 | it(@"should overwrite an existing default core when a new one is set", ^{ 26 | VENCore *oldCore = [[VENCore alloc] init]; 27 | [VENCore setDefaultCore:oldCore]; 28 | 29 | VENCore *newCore = [[VENCore alloc] init]; 30 | [VENCore setDefaultCore:newCore]; 31 | 32 | expect([VENCore defaultCore]).to.equal(newCore); 33 | }); 34 | 35 | it(@"should release an old core after setting a new core", ^{ 36 | #pragma diagnostic push 37 | #pragma clang diagnostic ignored "-Warc-unsafe-retained-assign" 38 | __weak VENCore *oldCore = [[VENCore alloc] init]; 39 | [VENCore setDefaultCore:oldCore]; 40 | expect([VENCore defaultCore]).to.equal(oldCore); 41 | 42 | [VENCore setDefaultCore:nil]; 43 | 44 | expect(oldCore).will.equal(nil); 45 | #pragma diagnostic pop 46 | 47 | }); 48 | 49 | it(@"should clear out the defaultCore when set to nil", ^{ 50 | VENCore *newCore = [[VENCore alloc] init]; 51 | [VENCore setDefaultCore:newCore]; 52 | [VENCore setDefaultCore:nil]; 53 | expect([VENCore defaultCore]).to.equal(nil); 54 | }); 55 | 56 | }); 57 | 58 | 59 | SpecEnd -------------------------------------------------------------------------------- /VENCoreUnitTests/VENCoreUnitTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /VENCoreUnitTests/VENCoreUnitTests-Prefix.pch: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @import XCTest; 3 | 4 | #define SPT_SHORTHAND 5 | #import 6 | 7 | #define EXP_SHORTHAND 8 | #import 9 | 10 | #import 11 | 12 | #define HC_SHORTHAND 13 | #import 14 | 15 | #import 16 | 17 | #import "VENTestUtilities.h" 18 | 19 | #import "EXPMatchers+Venmo.h" 20 | -------------------------------------------------------------------------------- /VENCoreUnitTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /docs/DESIGN.md: -------------------------------------------------------------------------------- 1 | ## VENCore 2 | 3 | ## Users 4 | * Venmo app / VenmoClient 5 | * VENAppSwitchSDK 6 | * VenmoSDK 7 | * Venmo 8 | 9 | ## Compatibility 10 | * Mac OS X 10.? 11 | * iOS 6+ 12 | * ARC Only 13 | 14 | ## Class hierarchy 15 | ### Public interface 16 | * `VENCore` - public facing interface 17 | * Responsible for: 18 | * Providing a simple interface for interacting with the Venmo internal API 19 | 20 | ### Models 21 | * Should we have type-safe representations of Venmo resources? (YES) 22 | * If we do, should we use [Mantle](https://github.com/MantleFramework/Mantle)? 23 | * Pros: 24 | * Removes a lot of JSON<->model boilerplate for transforming values 25 | * i.e. datestamp <-> NSDate, string <-> enum 26 | * Mantle could be a transition layer between the Venmo API and Core Data 27 | * This [slide deck](http://www.slideshare.net/GuillermoGonzalez51/better-web-clients-with-mantle-and-afnetworking) is a pretty good overview and shows what using Mantle + AFNetworking could look like. 28 | * `MTLManagedObjectAdapter` seems to make going from Mantle -> Core Data pretty easy 29 | * We could turn our existing models into `MTLModel` categories that conform to `` 30 | * Cons: 31 | * Additional dependency 32 | 33 | ### Service 34 | * `VENClientAPI` – translates raw HTTP responses into domain objects 35 | * Responsible for: 36 | * API endpoint names 37 | * access tokens 38 | * default headers 39 | * response parsing 40 | --------------------------------------------------------------------------------