aboutsummaryrefslogtreecommitdiffhomepage
path: root/Example/Auth/Tests/FIRAuthKeychainTests.m
diff options
context:
space:
mode:
Diffstat (limited to 'Example/Auth/Tests/FIRAuthKeychainTests.m')
-rw-r--r--Example/Auth/Tests/FIRAuthKeychainTests.m314
1 files changed, 314 insertions, 0 deletions
diff --git a/Example/Auth/Tests/FIRAuthKeychainTests.m b/Example/Auth/Tests/FIRAuthKeychainTests.m
new file mode 100644
index 0000000..374e868
--- /dev/null
+++ b/Example/Auth/Tests/FIRAuthKeychainTests.m
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Security/Security.h>
+#import <XCTest/XCTest.h>
+
+#import "FIRAuthKeychain.h"
+
+/** @var kAccountPrefix
+ @brief The keychain account prefix assumed by the tests.
+ */
+static NSString *const kAccountPrefix = @"firebase_auth_1_";
+
+/** @var kKey
+ @brief The key used in tests.
+ */
+static NSString *const kKey = @"ACCOUNT";
+
+/** @var kService
+ @brief The keychain service used in tests.
+ */
+static NSString *const kService = @"SERVICE";
+
+/** @var kOtherService
+ @brief Another keychain service used in tests.
+ */
+static NSString *const kOtherService = @"OTHER_SERVICE";
+
+/** @var kData
+ @brief A piece of keychain data used in tests.
+ */
+static NSString *const kData = @"DATA";
+
+/** @var kOtherData
+ @brief Another piece of keychain data used in tests.
+ */
+static NSString *const kOtherData = @"OTHER_DATA";
+
+/** @fn accountFromKey
+ @brief Converts a key string to an account string.
+ @param key The key string to be converted from.
+ @return The account string being the conversion result.
+ */
+static NSString *accountFromKey(NSString *key) {
+ return [kAccountPrefix stringByAppendingString:key];
+}
+
+/** @fn dataFromString
+ @brief Converts a NSString to NSData.
+ @param string The NSString to be converted from.
+ @return The NSData being the conversion result.
+ */
+static NSData *dataFromString(NSString *string) {
+ return [string dataUsingEncoding:NSUTF8StringEncoding];
+}
+
+/** @fn stringFromData
+ @brief Converts a NSData to NSString.
+ @param data The NSData to be converted from.
+ @return The NSString being the conversion result.
+ */
+static NSString *stringFromData(NSData *data) {
+ return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+}
+
+/** @fn fakeError
+ @brief Creates a fake error object.
+ @return a non-nil NSError instance.
+ */
+static NSError *fakeError() {
+ return [NSError errorWithDomain:@"ERROR" code:-1 userInfo:nil];
+}
+
+/** @class FIRAuthKeychainTests
+ @brief Tests for @c FIRAuthKeychainTests .
+ */
+@interface FIRAuthKeychainTests : XCTestCase
+@end
+
+@implementation FIRAuthKeychainTests
+
+/** @fn testReadNonexisting
+ @brief Tests reading non-existing keychain item.
+ */
+- (void)testReadNonexisting {
+ [self setPassword:nil account:accountFromKey(kKey) service:kService];
+ [self setPassword:nil account:kKey service:nil]; // legacy form
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ NSError *error = fakeError();
+ XCTAssertNil([keychain dataForKey:kKey error:&error]);
+ XCTAssertNil(error);
+}
+
+/** @fn testReadExisting
+ @brief Tests reading existing keychain item.
+ */
+- (void)testReadExisting {
+ [self setPassword:kData account:accountFromKey(kKey) service:kService];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ NSError *error = fakeError();
+ XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
+ XCTAssertNil(error);
+ [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
+}
+
+/** @fn testNotReadOtherService
+ @brief Tests not reading keychain item belonging to other service.
+ */
+- (void)testNotReadOtherService {
+ [self setPassword:nil account:accountFromKey(kKey) service:kService];
+ [self setPassword:kData account:accountFromKey(kKey) service:kOtherService];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ NSError *error = fakeError();
+ XCTAssertNil([keychain dataForKey:kKey error:&error]);
+ XCTAssertNil(error);
+ [self deletePasswordWithAccount:accountFromKey(kKey) service:kOtherService];
+}
+
+/** @fn testWriteNonexisting
+ @brief Tests writing new keychain item.
+ */
+- (void)testWriteNonexisting {
+ [self setPassword:nil account:accountFromKey(kKey) service:kService];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ XCTAssertTrue([keychain setData:dataFromString(kData) forKey:kKey error:NULL]);
+ XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
+ [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
+}
+
+/** @fn testWriteExisting
+ @brief Tests overwriting existing keychain item.
+ */
+- (void)testWriteExisting {
+ [self setPassword:kData account:accountFromKey(kKey) service:kService];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ XCTAssertTrue([keychain setData:dataFromString(kOtherData) forKey:kKey error:NULL]);
+ XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService],
+ kOtherData);
+ [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
+}
+
+/** @fn testDeleteNonexisting
+ @brief Tests deleting non-existing keychain item.
+ */
+- (void)testDeleteNonexisting {
+ [self setPassword:nil account:accountFromKey(kKey) service:kService];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
+ XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
+}
+
+/** @fn testDeleteExisting
+ @brief Tests deleting existing keychain item.
+ */
+- (void)testDeleteExisting {
+ [self setPassword:kData account:accountFromKey(kKey) service:kService];
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
+ XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
+}
+
+/** @fn testReadLegacy
+ @brief Tests reading legacy keychain item.
+ */
+- (void)testReadLegacy {
+ [self setPassword:nil account:accountFromKey(kKey) service:kService];
+ [self setPassword:kData account:kKey service:nil]; // legacy form
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ NSError *error = fakeError();
+ XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
+ XCTAssertNil(error);
+ // Legacy item should have been moved to current form.
+ XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
+ XCTAssertNil([self passwordWithAccount:kKey service:nil]);
+ [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
+}
+
+/** @fn testNotReadLegacy
+ @brief Tests not reading legacy keychain item because current keychain item exists.
+ */
+- (void)testNotReadLegacy {
+ [self setPassword:kData account:accountFromKey(kKey) service:kService];
+ [self setPassword:kOtherData account:kKey service:nil]; // legacy form
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ NSError *error = fakeError();
+ XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
+ XCTAssertNil(error);
+ // Legacy item should have leave untouched.
+ XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
+ XCTAssertEqualObjects([self passwordWithAccount:kKey service:nil], kOtherData);
+ [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
+ [self deletePasswordWithAccount:kKey service:nil];
+}
+
+/** @fn testRemoveLegacy
+ @brief Tests removing keychain item also removes legacy keychain item.
+ */
+- (void)testRemoveLegacy {
+ [self setPassword:kData account:accountFromKey(kKey) service:kService];
+ [self setPassword:kOtherData account:kKey service:nil]; // legacy form
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
+ XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
+ XCTAssertNil([self passwordWithAccount:kKey service:nil]);
+}
+
+/** @fn testNullErrorParameter
+ @brief Tests that 'NULL' can be safely passed in.
+ */
+- (void)testNullErrorParameter {
+ FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
+ [keychain dataForKey:kKey error:NULL];
+ [keychain setData:dataFromString(kData) forKey:kKey error:NULL];
+ [keychain removeDataForKey:kKey error:NULL];
+}
+
+#pragma mark - Helpers
+
+/** @fn passwordWithAccount:service:
+ @brief Reads a generic password string from the keychain.
+ @param account The account attribute of the keychain item.
+ @param service The service attribute of the keychain item, if provided.
+ @return The generic password string, if the keychain item exists.
+ */
+- (nullable NSString *)passwordWithAccount:(nonnull NSString *)account
+ service:(nullable NSString *)service {
+ NSMutableDictionary *query = [@{
+ (__bridge id)kSecReturnData : @YES,
+ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
+ (__bridge id)kSecAttrAccount : account,
+ } mutableCopy];
+ if (service) {
+ query[(__bridge id)kSecAttrService] = service;
+ }
+ CFDataRef result;
+ OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
+ if (status == errSecItemNotFound) {
+ return nil;
+ }
+ XCTAssertEqual(status, errSecSuccess);
+ return stringFromData((__bridge NSData *)(result));
+}
+
+/** @fn addPassword:account:service:
+ @brief Adds a generic password string to the keychain.
+ @param password The value attribute for the password to write to the keychain item.
+ @param account The account attribute of the keychain item.
+ @param service The service attribute of the keychain item, if provided.
+ */
+- (void)addPassword:(nonnull NSString *)password
+ account:(nonnull NSString *)account
+ service:(nullable NSString *)service {
+ NSMutableDictionary *query = [@{
+ (__bridge id)kSecValueData : dataFromString(password),
+ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
+ (__bridge id)kSecAttrAccount : account,
+ } mutableCopy];
+ if (service) {
+ query[(__bridge id)kSecAttrService] = service;
+ }
+ OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
+ XCTAssertEqual(status, errSecSuccess);
+}
+
+/** @fn deletePasswordWithAccount:service:
+ @brief Deletes a generic password string from the keychain.
+ @param account The account attribute of the keychain item.
+ @param service The service attribute of the keychain item, if provided.
+ */
+- (void)deletePasswordWithAccount:(nonnull NSString *)account
+ service:(nullable NSString *)service {
+ NSMutableDictionary *query = [@{
+ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
+ (__bridge id)kSecAttrAccount : account,
+ } mutableCopy];
+ if (service) {
+ query[(__bridge id)kSecAttrService] = service;
+ }
+ OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
+ XCTAssertEqual(status, errSecSuccess);
+}
+
+/** @fn setPasswordWithString:account:service:
+ @brief Sets a generic password string to the keychain.
+ @param password The value attribute of the keychain item, if provided, or nil to delete the
+ existing password if any.
+ @param account The account attribute of the keychain item.
+ @param service The service attribute of the keychain item, if provided.
+ */
+- (void)setPassword:(nullable NSString *)password
+ account:(nonnull NSString *)account
+ service:(nullable NSString *)service {
+ if ([self passwordWithAccount:account service:service]) {
+ [self deletePasswordWithAccount:account service:service];
+ }
+ if (password) {
+ [self addPassword:password account:account service:service];
+ }
+}
+
+@end