Added ObjC fallbackKey support and updated tests.
This commit is contained in:
parent
8ddb72cfed
commit
91a619b745
3 changed files with 144 additions and 65 deletions
|
@ -35,6 +35,9 @@
|
||||||
/** Public parts of the unpublished one time keys for the account */
|
/** Public parts of the unpublished one time keys for the account */
|
||||||
- (NSDictionary*) oneTimeKeys;
|
- (NSDictionary*) oneTimeKeys;
|
||||||
|
|
||||||
|
/** Public part of the unpublished fallback key for the account */
|
||||||
|
- (NSDictionary*) fallbackKey;
|
||||||
|
|
||||||
- (BOOL) removeOneTimeKeysForSession:(OLMSession*)session;
|
- (BOOL) removeOneTimeKeysForSession:(OLMSession*)session;
|
||||||
|
|
||||||
/** Marks the current set of one time keys as being published. */
|
/** Marks the current set of one time keys as being published. */
|
||||||
|
@ -48,4 +51,7 @@
|
||||||
* discarded. */
|
* discarded. */
|
||||||
- (void) generateOneTimeKeys:(NSUInteger)numberOfKeys;
|
- (void) generateOneTimeKeys:(NSUInteger)numberOfKeys;
|
||||||
|
|
||||||
|
/** Generates a fallback key. */
|
||||||
|
- (void) generateFallbackKey;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -145,7 +145,6 @@
|
||||||
return keysDictionary;
|
return keysDictionary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void) generateOneTimeKeys:(NSUInteger)numberOfKeys {
|
- (void) generateOneTimeKeys:(NSUInteger)numberOfKeys {
|
||||||
size_t randomLength = olm_account_generate_one_time_keys_random_length(_account, numberOfKeys);
|
size_t randomLength = olm_account_generate_one_time_keys_random_length(_account, numberOfKeys);
|
||||||
NSMutableData *random = [OLMUtility randomBytesOfLength:randomLength];
|
NSMutableData *random = [OLMUtility randomBytesOfLength:randomLength];
|
||||||
|
@ -157,6 +156,40 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSDictionary *) fallbackKey {
|
||||||
|
size_t fallbackKeyLength = olm_account_fallback_key_length(_account);
|
||||||
|
uint8_t *fallbackKeyBytes = malloc(fallbackKeyLength);
|
||||||
|
if (!fallbackKeyBytes) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t result = olm_account_fallback_key(_account, fallbackKeyBytes, fallbackKeyLength);
|
||||||
|
if (result == olm_error()) {
|
||||||
|
const char *error = olm_account_last_error(_account);
|
||||||
|
NSLog(@"error getting fallback key: %s", error);
|
||||||
|
free(fallbackKeyBytes);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSData *fallbackKeyData = [NSData dataWithBytesNoCopy:fallbackKeyBytes length:fallbackKeyLength freeWhenDone:YES];
|
||||||
|
NSError *error = nil;
|
||||||
|
NSDictionary *keyDictionary = [NSJSONSerialization JSONObjectWithData:fallbackKeyData options:0 error:&error];
|
||||||
|
if (error) {
|
||||||
|
NSLog(@"Could not decode JSON: %@", error.localizedDescription);
|
||||||
|
}
|
||||||
|
return keyDictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) generateFallbackKey {
|
||||||
|
size_t randomLength = olm_account_generate_fallback_key_random_length(_account);
|
||||||
|
NSMutableData *random = [OLMUtility randomBytesOfLength:randomLength];
|
||||||
|
size_t result = olm_account_generate_fallback_key(_account, random.mutableBytes, random.length);
|
||||||
|
[random resetBytesInRange:NSMakeRange(0, random.length)];
|
||||||
|
if (result == olm_error()) {
|
||||||
|
const char *error = olm_account_last_error(_account);
|
||||||
|
NSLog(@"error generating keys: %s", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- (BOOL) removeOneTimeKeysForSession:(OLMSession *)session {
|
- (BOOL) removeOneTimeKeysForSession:(OLMSession *)session {
|
||||||
NSParameterAssert(session != nil);
|
NSParameterAssert(session != nil);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
|
|
@ -25,35 +25,38 @@ limitations under the License.
|
||||||
|
|
||||||
@implementation OLMKitTests
|
@implementation OLMKitTests
|
||||||
|
|
||||||
- (void)setUp {
|
|
||||||
[super setUp];
|
|
||||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)tearDown {
|
|
||||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
|
||||||
[super tearDown];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)testAliceAndBob {
|
- (void)testAliceAndBob {
|
||||||
NSError *error;
|
|
||||||
|
|
||||||
OLMAccount *alice = [[OLMAccount alloc] initNewAccount];
|
|
||||||
OLMAccount *bob = [[OLMAccount alloc] initNewAccount];
|
OLMAccount *bob = [[OLMAccount alloc] initNewAccount];
|
||||||
[bob generateOneTimeKeys:5];
|
[bob generateOneTimeKeys:5];
|
||||||
NSDictionary *bobIdKeys = bob.identityKeys;
|
|
||||||
NSString *bobIdKey = bobIdKeys[@"curve25519"];
|
|
||||||
NSDictionary *bobOneTimeKeys = bob.oneTimeKeys;
|
|
||||||
NSParameterAssert(bobIdKey != nil);
|
|
||||||
NSParameterAssert(bobOneTimeKeys != nil);
|
|
||||||
__block NSString *bobOneTimeKey = nil;
|
|
||||||
NSDictionary *bobOtkCurve25519 = bobOneTimeKeys[@"curve25519"];
|
|
||||||
[bobOtkCurve25519 enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
|
|
||||||
bobOneTimeKey = obj;
|
|
||||||
}];
|
|
||||||
XCTAssert([bobOneTimeKey isKindOfClass:[NSString class]]);
|
|
||||||
|
|
||||||
OLMSession *aliceSession = [[OLMSession alloc] initOutboundSessionWithAccount:alice theirIdentityKey:bobIdKey theirOneTimeKey:bobOneTimeKey error:nil];
|
[self _testAliceAndBob:bob withBobKeys:bob.oneTimeKeys];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testAliceAndBobFallbackKey {
|
||||||
|
OLMAccount *bob = [[OLMAccount alloc] initNewAccount];
|
||||||
|
[bob generateFallbackKey];
|
||||||
|
|
||||||
|
[self _testAliceAndBob:bob withBobKeys:bob.fallbackKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)_testAliceAndBob:(OLMAccount *)bob withBobKeys:(NSDictionary *)bobKeys {
|
||||||
|
XCTAssertNotNil(bob);
|
||||||
|
XCTAssertNotNil(bobKeys);
|
||||||
|
|
||||||
|
NSError *error;
|
||||||
|
|
||||||
|
NSString *bobIdKey = bob.identityKeys[@"curve25519"];
|
||||||
|
XCTAssertNotNil(bobIdKey);
|
||||||
|
|
||||||
|
__block NSString *bobKeyValue = nil;
|
||||||
|
[bobKeys[@"curve25519"] enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
|
||||||
|
bobKeyValue = obj;
|
||||||
|
}];
|
||||||
|
XCTAssert([bobKeyValue isKindOfClass:[NSString class]]);
|
||||||
|
|
||||||
|
OLMAccount *alice = [[OLMAccount alloc] initNewAccount];
|
||||||
|
|
||||||
|
OLMSession *aliceSession = [[OLMSession alloc] initOutboundSessionWithAccount:alice theirIdentityKey:bobIdKey theirOneTimeKey:bobKeyValue error:nil];
|
||||||
NSString *message = @"Hello!";
|
NSString *message = @"Hello!";
|
||||||
OLMMessage *aliceToBobMsg = [aliceSession encryptMessage:message error:&error];
|
OLMMessage *aliceToBobMsg = [aliceSession encryptMessage:message error:&error];
|
||||||
XCTAssertNil(error);
|
XCTAssertNil(error);
|
||||||
|
@ -75,29 +78,43 @@ limitations under the License.
|
||||||
XCTAssertTrue(success);
|
XCTAssertTrue(success);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) testBackAndForth {
|
- (void)testBackAndForthWithOneTimeKeys {
|
||||||
OLMAccount *alice = [[OLMAccount alloc] initNewAccount];
|
|
||||||
OLMAccount *bob = [[OLMAccount alloc] initNewAccount];
|
OLMAccount *bob = [[OLMAccount alloc] initNewAccount];
|
||||||
[bob generateOneTimeKeys:1];
|
[bob generateOneTimeKeys:1];
|
||||||
NSDictionary *bobIdKeys = bob.identityKeys;
|
|
||||||
NSString *bobIdKey = bobIdKeys[@"curve25519"];
|
|
||||||
NSDictionary *bobOneTimeKeys = bob.oneTimeKeys;
|
|
||||||
NSParameterAssert(bobIdKey != nil);
|
|
||||||
NSParameterAssert(bobOneTimeKeys != nil);
|
|
||||||
__block NSString *bobOneTimeKey = nil;
|
|
||||||
NSDictionary *bobOtkCurve25519 = bobOneTimeKeys[@"curve25519"];
|
|
||||||
[bobOtkCurve25519 enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
|
|
||||||
bobOneTimeKey = obj;
|
|
||||||
}];
|
|
||||||
XCTAssert([bobOneTimeKey isKindOfClass:[NSString class]]);
|
|
||||||
|
|
||||||
OLMSession *aliceSession = [[OLMSession alloc] initOutboundSessionWithAccount:alice theirIdentityKey:bobIdKey theirOneTimeKey:bobOneTimeKey error:nil];
|
[self _testBackAndForthWithBob:bob andBobKeys:bob.oneTimeKeys];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testBackAndForthWithFallbackKey {
|
||||||
|
OLMAccount *bob = [[OLMAccount alloc] initNewAccount];
|
||||||
|
[bob generateFallbackKey];
|
||||||
|
|
||||||
|
[self _testBackAndForthWithBob:bob andBobKeys:bob.fallbackKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)_testBackAndForthWithBob:(OLMAccount *)bob andBobKeys:(NSDictionary *)bobKeys {
|
||||||
|
XCTAssertNotNil(bob);
|
||||||
|
XCTAssertNotNil(bobKeys);
|
||||||
|
|
||||||
|
NSString *bobIdKey = bob.identityKeys[@"curve25519"];
|
||||||
|
XCTAssertNotNil(bobIdKey);
|
||||||
|
|
||||||
|
__block NSString *bobKeyValue = nil;
|
||||||
|
[bobKeys[@"curve25519"] enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
|
||||||
|
bobKeyValue = obj;
|
||||||
|
}];
|
||||||
|
XCTAssert([bobKeyValue isKindOfClass:[NSString class]]);
|
||||||
|
|
||||||
|
OLMAccount *alice = [[OLMAccount alloc] initNewAccount];
|
||||||
|
|
||||||
|
OLMSession *aliceSession = [[OLMSession alloc] initOutboundSessionWithAccount:alice theirIdentityKey:bobIdKey theirOneTimeKey:bobKeyValue error:nil];
|
||||||
NSString *message = @"Hello I'm Alice!";
|
NSString *message = @"Hello I'm Alice!";
|
||||||
OLMMessage *aliceToBobMsg = [aliceSession encryptMessage:message error:nil];
|
OLMMessage *aliceToBobMsg = [aliceSession encryptMessage:message error:nil];
|
||||||
|
|
||||||
OLMSession *bobSession = [[OLMSession alloc] initInboundSessionWithAccount:bob oneTimeKeyMessage:aliceToBobMsg.ciphertext error:nil];
|
OLMSession *bobSession = [[OLMSession alloc] initInboundSessionWithAccount:bob oneTimeKeyMessage:aliceToBobMsg.ciphertext error:nil];
|
||||||
NSString *plaintext = [bobSession decryptMessage:aliceToBobMsg error:nil];
|
NSString *plaintext = [bobSession decryptMessage:aliceToBobMsg error:nil];
|
||||||
XCTAssertEqualObjects(message, plaintext);
|
XCTAssertEqualObjects(message, plaintext);
|
||||||
|
|
||||||
BOOL success = [bob removeOneTimeKeysForSession:bobSession];
|
BOOL success = [bob removeOneTimeKeysForSession:bobSession];
|
||||||
XCTAssertTrue(success);
|
XCTAssertTrue(success);
|
||||||
|
|
||||||
|
@ -117,49 +134,68 @@ limitations under the License.
|
||||||
XCTAssertEqualObjects(msg3, dMsg3);
|
XCTAssertEqualObjects(msg3, dMsg3);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) testAccountSerialization {
|
- (void)testAccountSerialization {
|
||||||
OLMAccount *bob = [[OLMAccount alloc] initNewAccount];
|
OLMAccount *bob = [[OLMAccount alloc] initNewAccount];
|
||||||
[bob generateOneTimeKeys:5];
|
[bob generateOneTimeKeys:5];
|
||||||
|
[bob generateFallbackKey];
|
||||||
NSDictionary *bobIdKeys = bob.identityKeys;
|
NSDictionary *bobIdKeys = bob.identityKeys;
|
||||||
NSDictionary *bobOneTimeKeys = bob.oneTimeKeys;
|
NSDictionary *bobOneTimeKeys = bob.oneTimeKeys;
|
||||||
|
NSDictionary *bobFallbackKey = bob.fallbackKey;
|
||||||
|
|
||||||
NSData *bobData = [NSKeyedArchiver archivedDataWithRootObject:bob];
|
NSError *error;
|
||||||
|
NSData *bobData = [NSKeyedArchiver archivedDataWithRootObject:bob requiringSecureCoding:NO error:&error];
|
||||||
|
XCTAssertNil(error);
|
||||||
|
|
||||||
|
OLMAccount *bob2 = [NSKeyedUnarchiver unarchivedObjectOfClass:[OLMAccount class] fromData:bobData error:&error];
|
||||||
|
XCTAssertNil(error);
|
||||||
|
|
||||||
OLMAccount *bob2 = [NSKeyedUnarchiver unarchiveObjectWithData:bobData];
|
|
||||||
NSDictionary *bobIdKeys2 = bob2.identityKeys;
|
NSDictionary *bobIdKeys2 = bob2.identityKeys;
|
||||||
NSDictionary *bobOneTimeKeys2 = bob2.oneTimeKeys;
|
NSDictionary *bobOneTimeKeys2 = bob2.oneTimeKeys;
|
||||||
|
NSDictionary *bobFallbackKey2 = bob2.fallbackKey;
|
||||||
|
|
||||||
XCTAssertEqualObjects(bobIdKeys, bobIdKeys2);
|
XCTAssertEqualObjects(bobIdKeys, bobIdKeys2);
|
||||||
XCTAssertEqualObjects(bobOneTimeKeys, bobOneTimeKeys2);
|
XCTAssertEqualObjects(bobOneTimeKeys, bobOneTimeKeys2);
|
||||||
|
XCTAssertEqualObjects(bobFallbackKey, bobFallbackKey2);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) testSessionSerialization {
|
- (void)testSessionSerializationWithOneTimeKey {
|
||||||
NSError *error;
|
|
||||||
|
|
||||||
OLMAccount *alice = [[OLMAccount alloc] initNewAccount];
|
|
||||||
OLMAccount *bob = [[OLMAccount alloc] initNewAccount];
|
OLMAccount *bob = [[OLMAccount alloc] initNewAccount];
|
||||||
[bob generateOneTimeKeys:1];
|
[bob generateOneTimeKeys:1];
|
||||||
NSDictionary *bobIdKeys = bob.identityKeys;
|
|
||||||
NSString *bobIdKey = bobIdKeys[@"curve25519"];
|
|
||||||
NSDictionary *bobOneTimeKeys = bob.oneTimeKeys;
|
|
||||||
NSParameterAssert(bobIdKey != nil);
|
|
||||||
NSParameterAssert(bobOneTimeKeys != nil);
|
|
||||||
__block NSString *bobOneTimeKey = nil;
|
|
||||||
NSDictionary *bobOtkCurve25519 = bobOneTimeKeys[@"curve25519"];
|
|
||||||
[bobOtkCurve25519 enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
|
|
||||||
bobOneTimeKey = obj;
|
|
||||||
}];
|
|
||||||
XCTAssert([bobOneTimeKey isKindOfClass:[NSString class]]);
|
|
||||||
|
|
||||||
OLMSession *aliceSession = [[OLMSession alloc] initOutboundSessionWithAccount:alice theirIdentityKey:bobIdKey theirOneTimeKey:bobOneTimeKey error:nil];
|
[self _testSessionSerializationWithBob:bob bobKeys:bob.oneTimeKeys];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testSessionSerializationWithFallbackKey {
|
||||||
|
OLMAccount *bob = [[OLMAccount alloc] initNewAccount];
|
||||||
|
[bob generateFallbackKey];
|
||||||
|
|
||||||
|
[self _testSessionSerializationWithBob:bob bobKeys:bob.fallbackKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)_testSessionSerializationWithBob:(OLMAccount *)bob bobKeys:(NSDictionary *)bobKeys {
|
||||||
|
NSError *error;
|
||||||
|
|
||||||
|
NSString *bobIdKey = bob.identityKeys[@"curve25519"];
|
||||||
|
XCTAssertNotNil(bobIdKey);
|
||||||
|
|
||||||
|
__block NSString *bobKeyValue = nil;
|
||||||
|
[bobKeys[@"curve25519"] enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
|
||||||
|
bobKeyValue = obj;
|
||||||
|
}];
|
||||||
|
XCTAssert([bobKeyValue isKindOfClass:[NSString class]]);
|
||||||
|
|
||||||
|
OLMAccount *alice = [[OLMAccount alloc] initNewAccount];
|
||||||
|
|
||||||
|
OLMSession *aliceSession = [[OLMSession alloc] initOutboundSessionWithAccount:alice theirIdentityKey:bobIdKey theirOneTimeKey:bobKeyValue error:nil];
|
||||||
NSString *message = @"Hello I'm Alice!";
|
NSString *message = @"Hello I'm Alice!";
|
||||||
|
|
||||||
OLMMessage *aliceToBobMsg = [aliceSession encryptMessage:message error:&error];
|
OLMMessage *aliceToBobMsg = [aliceSession encryptMessage:message error:&error];
|
||||||
XCTAssertNil(error);
|
XCTAssertNil(error);
|
||||||
|
|
||||||
|
|
||||||
OLMSession *bobSession = [[OLMSession alloc] initInboundSessionWithAccount:bob oneTimeKeyMessage:aliceToBobMsg.ciphertext error:nil];
|
OLMSession *bobSession = [[OLMSession alloc] initInboundSessionWithAccount:bob oneTimeKeyMessage:aliceToBobMsg.ciphertext error:nil];
|
||||||
NSString *plaintext = [bobSession decryptMessage:aliceToBobMsg error:nil];
|
NSString *plaintext = [bobSession decryptMessage:aliceToBobMsg error:nil];
|
||||||
XCTAssertEqualObjects(message, plaintext);
|
XCTAssertEqualObjects(message, plaintext);
|
||||||
|
|
||||||
BOOL success = [bob removeOneTimeKeysForSession:bobSession];
|
BOOL success = [bob removeOneTimeKeysForSession:bobSession];
|
||||||
XCTAssertTrue(success);
|
XCTAssertTrue(success);
|
||||||
|
|
||||||
|
@ -171,8 +207,11 @@ limitations under the License.
|
||||||
OLMMessage *eMsg2 = [bobSession encryptMessage:msg2 error:nil];
|
OLMMessage *eMsg2 = [bobSession encryptMessage:msg2 error:nil];
|
||||||
OLMMessage *eMsg3 = [bobSession encryptMessage:msg3 error:nil];
|
OLMMessage *eMsg3 = [bobSession encryptMessage:msg3 error:nil];
|
||||||
|
|
||||||
NSData *aliceData = [NSKeyedArchiver archivedDataWithRootObject:aliceSession];
|
NSData *aliceData = [NSKeyedArchiver archivedDataWithRootObject:aliceSession requiringSecureCoding:NO error:&error];
|
||||||
OLMSession *alice2 = [NSKeyedUnarchiver unarchiveObjectWithData:aliceData];
|
XCTAssertNil(error);
|
||||||
|
|
||||||
|
OLMSession *alice2 = [NSKeyedUnarchiver unarchivedObjectOfClass:[OLMSession class] fromData:aliceData error:&error];
|
||||||
|
XCTAssertNil(error);
|
||||||
|
|
||||||
NSString *dMsg1 = [alice2 decryptMessage:eMsg1 error:nil];
|
NSString *dMsg1 = [alice2 decryptMessage:eMsg1 error:nil];
|
||||||
NSString *dMsg2 = [alice2 decryptMessage:eMsg2 error:nil];
|
NSString *dMsg2 = [alice2 decryptMessage:eMsg2 error:nil];
|
||||||
|
@ -183,6 +222,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)testEd25519Signing {
|
- (void)testEd25519Signing {
|
||||||
|
NSError *error;
|
||||||
|
|
||||||
OLMUtility *olmUtility = [[OLMUtility alloc] init];
|
OLMUtility *olmUtility = [[OLMUtility alloc] init];
|
||||||
OLMAccount *alice = [[OLMAccount alloc] initNewAccount];
|
OLMAccount *alice = [[OLMAccount alloc] initNewAccount];
|
||||||
|
@ -191,13 +231,13 @@ limitations under the License.
|
||||||
@"key1": @"value1",
|
@"key1": @"value1",
|
||||||
@"key2": @"value2"
|
@"key2": @"value2"
|
||||||
};
|
};
|
||||||
NSData *message = [NSKeyedArchiver archivedDataWithRootObject:aJSON];
|
NSData *message = [NSKeyedArchiver archivedDataWithRootObject:aJSON requiringSecureCoding:NO error:&error];
|
||||||
|
XCTAssertNil(error);
|
||||||
|
|
||||||
NSString *signature = [alice signMessage:message];
|
NSString *signature = [alice signMessage:message];
|
||||||
|
|
||||||
|
|
||||||
NSString *aliceEd25519Key = alice.identityKeys[@"ed25519"];
|
NSString *aliceEd25519Key = alice.identityKeys[@"ed25519"];
|
||||||
|
|
||||||
NSError *error;
|
|
||||||
BOOL result = [olmUtility verifyEd25519Signature:signature key:aliceEd25519Key message:message error:&error];
|
BOOL result = [olmUtility verifyEd25519Signature:signature key:aliceEd25519Key message:message error:&error];
|
||||||
XCTAssert(result);
|
XCTAssert(result);
|
||||||
XCTAssertNil(error);
|
XCTAssertNil(error);
|
||||||
|
|
Loading…
Reference in a new issue