diff --git a/Podfile.lock b/Podfile.lock index 3ca93e031..3c6883d32 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -25,13 +25,13 @@ PODS: - libwebp/sharpyuv (1.3.2) - libwebp/webp (1.3.2): - libwebp/sharpyuv - - Nimble (10.0.0) + - Nimble (12.3.0) - NVActivityIndicatorView (5.1.1): - NVActivityIndicatorView/Base (= 5.1.1) - NVActivityIndicatorView/Base (5.1.1) - OpenSSL-Universal (1.1.1300) - PureLayout (3.1.9) - - Quick (5.0.1) + - Quick (7.3.0) - Reachability (3.2) - SAMKeychain (1.5.3) - SignalCoreKit (1.0.0): @@ -137,11 +137,9 @@ SPEC REPOS: - CocoaLumberjack - DifferenceKit - GRDB.swift - - Nimble - NVActivityIndicatorView - OpenSSL-Universal - PureLayout - - Quick - Reachability - SAMKeychain - SQLCipher @@ -149,6 +147,8 @@ SPEC REPOS: - WebRTC-lib trunk: - libwebp + - Nimble + - Quick - xcbeautify EXTERNAL SOURCES: @@ -190,11 +190,11 @@ SPEC CHECKSUMS: DifferenceKit: ab185c4d7f9cef8af3fcf593e5b387fb81e999ca GRDB.swift: fe420b1af49ec519c7e96e07887ee44f5dfa2b78 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 - Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84 + Nimble: f8a8219d16f176429b951e8f7e72df5c23ceddc0 NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667 OpenSSL-Universal: e7311447fd2419f57420c79524b641537387eff2 PureLayout: 5fb5e5429519627d60d079ccb1eaa7265ce7cf88 - Quick: 749aa754fd1e7d984f2000fe051e18a3a9809179 + Quick: d32871931c05547cb4e0bc9009d66a18b50d8558 Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SignalCoreKit: 1fbd8732163ef76de16cd1107d1fa3684b607e5d diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 1d057de96..d305dea61 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -575,7 +575,6 @@ FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; }; FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; }; FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; }; - FD2B4AFB29429D1000AB4848 /* ConfigContactsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4AFA29429D1000AB4848 /* ConfigContactsSpec.swift */; }; FD2B4AFD294688D000AB4848 /* SessionUtil+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4AFC294688D000AB4848 /* SessionUtil+Contacts.swift */; }; FD2B4AFF2946C93200AB4848 /* ConfigurationSyncJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */; }; FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */; }; @@ -726,7 +725,6 @@ FD8ECF7B29340FFD00C0D1BB /* SessionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */; }; FD8ECF7D2934293A00C0D1BB /* _013_SessionUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */; }; FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7E2934298100C0D1BB /* ConfigDump.swift */; }; - FD8ECF822934387A00C0D1BB /* ConfigUserProfileSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */; }; FD8ECF892935AB7200C0D1BB /* SessionUtilError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF882935AB7200C0D1BB /* SessionUtilError.swift */; }; FD8ECF8B2935DB4B00C0D1BB /* SharedConfigMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF8A2935DB4B00C0D1BB /* SharedConfigMessage.swift */; }; FD8ECF9029381FC200C0D1BB /* SessionUtil+UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF8F29381FC200C0D1BB /* SessionUtil+UserProfile.swift */; }; @@ -747,7 +745,6 @@ FD9DD2712A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; FD9DD2722A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; - FDA1E83629A5748F00C5C3BD /* ConfigUserGroupsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83529A5748F00C5C3BD /* ConfigUserGroupsSpec.swift */; }; FDA1E83929A5771A00C5C3BD /* LibSessionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83829A5771A00C5C3BD /* LibSessionSpec.swift */; }; FDA1E83B29A5F2D500C5C3BD /* SessionUtil+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83A29A5F2D500C5C3BD /* SessionUtil+Shared.swift */; }; FDA1E83D29AC71A800C5C3BD /* SessionUtilSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83C29AC71A800C5C3BD /* SessionUtilSpec.swift */; }; @@ -758,7 +755,6 @@ FDB4BBC92839BEF000B7C95D /* ProfileManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB4BBC82839BEF000B7C95D /* ProfileManagerError.swift */; }; FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */; }; FDB7400D28EBEC240094D718 /* DateHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */; }; - FDBB25E12983909300F1508E /* ConfigConvoInfoVolatileSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E02983909300F1508E /* ConfigConvoInfoVolatileSpec.swift */; }; FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */; }; FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */; }; FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */; }; @@ -921,6 +917,11 @@ FDFDE126282D05380098B17F /* MediaInteractiveDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE125282D05380098B17F /* MediaInteractiveDismiss.swift */; }; FDFDE128282D05530098B17F /* MediaPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE127282D05530098B17F /* MediaPresentationContext.swift */; }; FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */; }; + FDFE75B12ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */; }; + FDFE75B22ABD469500655640 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; }; + FDFE75B32ABD469500655640 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; }; + FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; + FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFF61D729F2600300F95FB0 /* Identity+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */; }; FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */; }; FE5FDED6D91BB4B3FA5C104D /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A9C113D2086D3C8A68A371C /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */; }; @@ -1697,7 +1698,6 @@ FD29598F2A43BE5F00888A17 /* VersionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionSpec.swift; sourceTree = ""; }; FD2959912A4417A900888A17 /* PreparedSendData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreparedSendData.swift; sourceTree = ""; }; FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronousStorage.swift; sourceTree = ""; }; - FD2B4AFA29429D1000AB4848 /* ConfigContactsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigContactsSpec.swift; sourceTree = ""; }; FD2B4AFC294688D000AB4848 /* SessionUtil+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+Contacts.swift"; sourceTree = ""; }; FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationSyncJob.swift; sourceTree = ""; }; FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = ""; }; @@ -1846,7 +1846,6 @@ FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionUtil.swift; sourceTree = ""; }; FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_SessionUtilChanges.swift; sourceTree = ""; }; FD8ECF7E2934298100C0D1BB /* ConfigDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigDump.swift; sourceTree = ""; }; - FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigUserProfileSpec.swift; sourceTree = ""; }; FD8ECF882935AB7200C0D1BB /* SessionUtilError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionUtilError.swift; sourceTree = ""; }; FD8ECF8A2935DB4B00C0D1BB /* SharedConfigMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedConfigMessage.swift; sourceTree = ""; }; FD8ECF8F29381FC200C0D1BB /* SessionUtil+UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+UserProfile.swift"; sourceTree = ""; }; @@ -1861,7 +1860,6 @@ FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = ""; }; FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSessionUtil.a; sourceTree = BUILT_PRODUCTS_DIR; }; FD9DD2702A72516D00ECB68E /* TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestExtensions.swift; sourceTree = ""; }; - FDA1E83529A5748F00C5C3BD /* ConfigUserGroupsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigUserGroupsSpec.swift; sourceTree = ""; }; FDA1E83829A5771A00C5C3BD /* LibSessionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionSpec.swift; sourceTree = ""; }; FDA1E83A29A5F2D500C5C3BD /* SessionUtil+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+Shared.swift"; sourceTree = ""; }; FDA1E83C29AC71A800C5C3BD /* SessionUtilSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionUtilSpec.swift; sourceTree = ""; }; @@ -1872,7 +1870,6 @@ FDB4BBC82839BEF000B7C95D /* ProfileManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManagerError.swift; sourceTree = ""; }; FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Utilities.swift"; sourceTree = ""; }; FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateHeaderCell.swift; sourceTree = ""; }; - FDBB25E02983909300F1508E /* ConfigConvoInfoVolatileSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigConvoInfoVolatileSpec.swift; sourceTree = ""; }; FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_AddJobPriority.swift; sourceTree = ""; }; FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = ""; }; FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeRequest.swift; sourceTree = ""; }; @@ -2038,6 +2035,7 @@ FDFDE125282D05380098B17F /* MediaInteractiveDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInteractiveDismiss.swift; sourceTree = ""; }; FDFDE127282D05530098B17F /* MediaPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPresentationContext.swift; sourceTree = ""; }; FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaZoomAnimationController.swift; sourceTree = ""; }; + FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _016_MakeBrokenProfileTimestampsNullable.swift; sourceTree = ""; }; FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Identity+Utilities.swift"; sourceTree = ""; }; FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEncoder+Utilities.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -3616,6 +3614,7 @@ FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */, FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */, FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */, + FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */, ); path = Migrations; sourceTree = ""; @@ -4044,6 +4043,7 @@ FD23CE272A67755C0000B97C /* MockCrypto.swift */, FD23CE2B2A678DF80000B97C /* MockCaches.swift */, FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */, + FD83B9D127D59495005E1583 /* MockUserDefaults.swift */, FD23CE312A67C38D0000B97C /* MockNetwork.swift */, FD96F3A629DBD43D00401309 /* MockJobRunner.swift */, FD83B9BD27CF2243005E1583 /* TestConstants.swift */, @@ -4095,7 +4095,6 @@ FD8ECF802934385900C0D1BB /* LibSessionUtil */ = { isa = PBXGroup; children = ( - FDA1E83729A5770C00C5C3BD /* Configs */, FDDC08F029A300D500BF9681 /* Utilities */, FDA1E83829A5771A00C5C3BD /* LibSessionSpec.swift */, FDA1E83C29AC71A800C5C3BD /* SessionUtilSpec.swift */, @@ -4148,17 +4147,6 @@ path = Networking; sourceTree = ""; }; - FDA1E83729A5770C00C5C3BD /* Configs */ = { - isa = PBXGroup; - children = ( - FD2B4AFA29429D1000AB4848 /* ConfigContactsSpec.swift */, - FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */, - FDBB25E02983909300F1508E /* ConfigConvoInfoVolatileSpec.swift */, - FDA1E83529A5748F00C5C3BD /* ConfigUserGroupsSpec.swift */, - ); - path = Configs; - sourceTree = ""; - }; FDC13D4E2A16EE41007267C7 /* Types */ = { isa = PBXGroup; children = ( @@ -4290,7 +4278,6 @@ isa = PBXGroup; children = ( FDC438BC27BB2AB400C60D73 /* Mockable.swift */, - FD83B9D127D59495005E1583 /* MockUserDefaults.swift */, FD078E4C27E17156000769AF /* MockOGMCache.swift */, ); path = _TestUtilities; @@ -5935,6 +5922,7 @@ C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */, FD245C642850664F00B966DD /* Threading.swift in Sources */, FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */, + FDFE75B12ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift in Sources */, C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */, FD09799B27FFC82D00936362 /* Quote.swift in Sources */, FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */, @@ -6193,9 +6181,11 @@ FD23EA5F28ED00FF0058676E /* CommonMockedExtensions.swift in Sources */, FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */, FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */, + FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */, FD0969FA2A6A00B000C5C365 /* Mocked.swift in Sources */, FD23EA5C28ED00F80058676E /* Mock.swift in Sources */, FD23EA6128ED0B260058676E /* CombineExtensions.swift in Sources */, + FDFE75B32ABD469500655640 /* MockJobRunner.swift in Sources */, FD9DD2712A72516D00ECB68E /* TestExtensions.swift in Sources */, FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */, ); @@ -6210,10 +6200,12 @@ FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, FDFBB7542A2023EB00CA7350 /* BencodeSpec.swift in Sources */, FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */, + FDFE75B22ABD469500655640 /* MockJobRunner.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */, FD23CE352A67C4DA0000B97C /* MockNetwork.swift in Sources */, + FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */, FD23CE282A67755C0000B97C /* MockCrypto.swift in Sources */, FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */, FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */, @@ -6240,7 +6232,6 @@ FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */, FD3C906A27E417CE00CD579F /* CryptoSMKSpec.swift in Sources */, FD96F3A729DBD43D00401309 /* MockJobRunner.swift in Sources */, - FDBB25E12983909300F1508E /* ConfigConvoInfoVolatileSpec.swift in Sources */, FD3C907127E445E500CD579F /* MessageReceiverDecryptionSpec.swift in Sources */, FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */, FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */, @@ -6253,9 +6244,7 @@ FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */, FD078E4D27E17156000769AF /* MockOGMCache.swift in Sources */, FD9DD2722A72516D00ECB68E /* TestExtensions.swift in Sources */, - FDA1E83629A5748F00C5C3BD /* ConfigUserGroupsSpec.swift in Sources */, FDC290A627D860CE005DAE71 /* Mock.swift in Sources */, - FD2B4AFB29429D1000AB4848 /* ConfigContactsSpec.swift in Sources */, FDA1E83D29AC71A800C5C3BD /* SessionUtilSpec.swift in Sources */, FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */, FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */, @@ -6264,7 +6253,6 @@ FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */, FD23CE342A67C4D90000B97C /* MockNetwork.swift in Sources */, FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */, - FD8ECF822934387A00C0D1BB /* ConfigUserProfileSpec.swift in Sources */, FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */, FD0969F92A69FFE700C5C365 /* Mocked.swift in Sources */, diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 5246a9687..5f8edc944 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -151,7 +151,7 @@ class ThreadSettingsViewModel: SessionTableViewModel Bool { - guard let serverUrl: URL = URL(string: server.lowercased()) else { return false } + guard let serverUrl: URL = (URL(string: server.lowercased()) ?? URL(string: "http://\(server.lowercased())")) else { + return false + } let serverPort: String = OpenGroupManager.port(for: server, serverUrl: serverUrl) let serverHost: String = serverUrl.host diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift index 4e07e7aad..adc7a7ea9 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift @@ -58,14 +58,14 @@ internal extension SessionUtil { let profileNameShouldBeUpdated: Bool = ( !data.profile.name.isEmpty && profile.name != data.profile.name && - profile.lastNameUpdate < data.profile.lastNameUpdate + (profile.lastNameUpdate ?? 0) < (data.profile.lastNameUpdate ?? 0) ) let profilePictureShouldBeUpdated: Bool = ( ( profile.profilePictureUrl != data.profile.profilePictureUrl || profile.profileEncryptionKey != data.profile.profileEncryptionKey ) && - profile.lastProfilePictureUpdate < data.profile.lastProfilePictureUpdate + (profile.lastProfilePictureUpdate ?? 0) < (data.profile.lastProfilePictureUpdate ?? 0) ) if diff --git a/SessionMessagingKit/Utilities/ProfileManager.swift b/SessionMessagingKit/Utilities/ProfileManager.swift index 7c206107d..24192128f 100644 --- a/SessionMessagingKit/Utilities/ProfileManager.swift +++ b/SessionMessagingKit/Utilities/ProfileManager.swift @@ -510,7 +510,7 @@ public struct ProfileManager { // Name if let name: String = name, !name.isEmpty, name != profile.name { - if sentTimestamp > profile.lastNameUpdate || (isCurrentUser && calledFromConfigHandling) { + if sentTimestamp > (profile.lastNameUpdate ?? 0) || (isCurrentUser && calledFromConfigHandling) { profileChanges.append(Profile.Columns.name.set(to: name)) profileChanges.append(Profile.Columns.lastNameUpdate.set(to: sentTimestamp)) } @@ -526,7 +526,7 @@ public struct ProfileManager { var avatarNeedsDownload: Bool = false var targetAvatarUrl: String? = nil - if sentTimestamp > profile.lastProfilePictureUpdate || (isCurrentUser && calledFromConfigHandling) { + if sentTimestamp > (profile.lastProfilePictureUpdate ?? 0) || (isCurrentUser && calledFromConfigHandling) { switch avatarUpdate { case .none: break case .uploadImageData: preconditionFailure("Invalid options for this function") diff --git a/SessionMessagingKitTests/Common Networking/Models/FileUploadResponseSpec.swift b/SessionMessagingKitTests/Common Networking/Models/FileUploadResponseSpec.swift index 499b8e658..283978f2a 100644 --- a/SessionMessagingKitTests/Common Networking/Models/FileUploadResponseSpec.swift +++ b/SessionMessagingKitTests/Common Networking/Models/FileUploadResponseSpec.swift @@ -8,11 +8,12 @@ import Nimble @testable import SessionMessagingKit class FileUploadResponseSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - a FileUploadResponse describe("a FileUploadResponse") { + // MARK: -- when decoding context("when decoding") { + // MARK: ---- handles a string id value it("handles a string id value") { let jsonData: Data = "{\"id\":\"123\"}".data(using: .utf8)! let response: FileUploadResponse? = try? JSONDecoder().decode(FileUploadResponse.self, from: jsonData) @@ -20,6 +21,7 @@ class FileUploadResponseSpec: QuickSpec { expect(response?.id).to(equal("123")) } + // MARK: ---- handles an int id value it("handles an int id value") { let jsonData: Data = "{\"id\":124}".data(using: .utf8)! let response: FileUploadResponse? = try? JSONDecoder().decode(FileUploadResponse.self, from: jsonData) diff --git a/SessionMessagingKitTests/Contacts/BlindedIdLookupSpec.swift b/SessionMessagingKitTests/Contacts/BlindedIdLookupSpec.swift index 4cb4b4cba..f70c9f424 100644 --- a/SessionMessagingKitTests/Contacts/BlindedIdLookupSpec.swift +++ b/SessionMessagingKitTests/Contacts/BlindedIdLookupSpec.swift @@ -8,11 +8,12 @@ import Nimble @testable import SessionMessagingKit class BlindedIdLookupSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - a BlindedIdLookup describe("a BlindedIdLookup") { + // MARK: -- when initializing context("when initializing") { + // MARK: ---- sets the values correctly it("sets the values correctly") { let lookup: BlindedIdLookup = BlindedIdLookup( blindedId: "testBlindedId", diff --git a/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift index 3de0762cf..c6af18418 100644 --- a/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift @@ -10,49 +10,37 @@ import Nimble @testable import SessionUtilitiesKit class MessageSendJobSpec: QuickSpec { - // MARK: - Spec - - override func spec() { - var job: Job! - var interaction: Interaction! - var attachment: Attachment! - var interactionAttachment: InteractionAttachment! - var mockStorage: Storage! - var mockJobRunner: MockJobRunner! - var dependencies: Dependencies! + override class func spec() { + // MARK: Configuration - // MARK: - JobRunner - - describe("a MessageSendJob") { - // MARK: - Configuration - - beforeEach { - mockStorage = SynchronousStorage( - customWriter: try! DatabaseQueue(), - customMigrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ] + @TestState var job: Job! + @TestState var interaction: Interaction! + @TestState var attachment: Attachment! = Attachment( + id: "200", + variant: .standard, + state: .failedDownload, + contentType: "text/plain", + byteCount: 200 + ) + @TestState var interactionAttachment: InteractionAttachment! + @TestState var mockStorage: Storage! = SynchronousStorage( + customWriter: try! DatabaseQueue(), + customMigrationTargets: [ + SNUtilitiesKit.self, + SNMessagingKit.self + ], + initialData: { db in + try SessionThread.fetchOrCreate( + db, + id: "Test1", + variant: .contact, + shouldBeVisible: true ) - mockJobRunner = MockJobRunner() - dependencies = Dependencies( - storage: mockStorage, - jobRunner: mockJobRunner, - dateNow: Date(timeIntervalSince1970: 1234567890) - ) - attachment = Attachment( - id: "200", - variant: .standard, - state: .failedDownload, - contentType: "text/plain", - byteCount: 200 - ) - - mockStorage.write { db in - try SessionThread.fetchOrCreate(db, id: "Test1", variant: .contact, shouldBeVisible: true) - } - - mockJobRunner + } + ) + @TestState var mockJobRunner: MockJobRunner! = MockJobRunner( + initialSetup: { jobRunner in + jobRunner .when { $0.jobInfoFor( jobs: nil, @@ -61,7 +49,7 @@ class MessageSendJobSpec: QuickSpec { ) } .thenReturn([:]) - mockJobRunner + jobRunner .when { $0.insert(any(), job: any(), before: any()) } .then { args in let db: Database = args[0] as! Database @@ -72,14 +60,16 @@ class MessageSendJobSpec: QuickSpec { } .thenReturn((1000, Job(variant: .messageSend))) } - - afterEach { - job = nil - mockStorage = nil - dependencies = nil - } - - // MARK: - fails when not given any details + ) + @TestState var dependencies: Dependencies! = Dependencies( + storage: mockStorage, + jobRunner: mockJobRunner, + dateNow: Date(timeIntervalSince1970: 1234567890) + ) + + // MARK: - a MessageSendJob + describe("a MessageSendJob") { + // MARK: -- fails when not given any details it("fails when not given any details") { job = Job(variant: .messageSend) @@ -102,7 +92,7 @@ class MessageSendJobSpec: QuickSpec { expect(permanentFailure).to(beTrue()) } - // MARK: - fails when given incorrect details + // MARK: -- fails when given incorrect details it("fails when given incorrect details") { job = Job( variant: .messageSend, @@ -128,7 +118,7 @@ class MessageSendJobSpec: QuickSpec { expect(permanentFailure).to(beTrue()) } - // MARK: - of VisibleMessage + // MARK: -- of VisibleMessage context("of VisibleMessage") { beforeEach { interaction = Interaction( @@ -167,7 +157,7 @@ class MessageSendJobSpec: QuickSpec { } } - // MARK: -- fails when there is no job id + // MARK: ---- fails when there is no job id it("fails when there is no job id") { job = Job( variant: .messageSend, @@ -199,7 +189,7 @@ class MessageSendJobSpec: QuickSpec { expect(permanentFailure).to(beTrue()) } - // MARK: -- fails when there is no interaction id + // MARK: ---- fails when there is no interaction id it("fails when there is no interaction id") { job = Job( variant: .messageSend, @@ -230,7 +220,7 @@ class MessageSendJobSpec: QuickSpec { expect(permanentFailure).to(beTrue()) } - // MARK: -- fails when there is no interaction for the provided interaction id + // MARK: ---- fails when there is no interaction for the provided interaction id it("fails when there is no interaction for the provided interaction id") { job = Job( variant: .messageSend, @@ -263,7 +253,7 @@ class MessageSendJobSpec: QuickSpec { expect(permanentFailure).to(beTrue()) } - // MARK: -- with an attachment + // MARK: ---- with an attachment context("with an attachment") { beforeEach { interactionAttachment = InteractionAttachment( @@ -278,7 +268,7 @@ class MessageSendJobSpec: QuickSpec { } } - // MARK: ---- it fails when trying to send with an attachment which previously failed to download + // MARK: ------ it fails when trying to send with an attachment which previously failed to download it("it fails when trying to send with an attachment which previously failed to download") { mockStorage.write { db in try attachment.with(state: .failedDownload).save(db) @@ -303,7 +293,7 @@ class MessageSendJobSpec: QuickSpec { expect(permanentFailure).to(beTrue()) } - // MARK: ---- with a pending upload + // MARK: ------ with a pending upload context("with a pending upload") { beforeEach { mockStorage.write { db in @@ -311,7 +301,7 @@ class MessageSendJobSpec: QuickSpec { } } - // MARK: ------ it defers when trying to send with an attachment which is still pending upload + // MARK: -------- it defers when trying to send with an attachment which is still pending upload it("it defers when trying to send with an attachment which is still pending upload") { var didDefer: Bool = false @@ -331,7 +321,7 @@ class MessageSendJobSpec: QuickSpec { expect(didDefer).to(beTrue()) } - // MARK: ------ it defers when trying to send with an uploaded attachment that has an invalid downloadUrl + // MARK: -------- it defers when trying to send with an uploaded attachment that has an invalid downloadUrl it("it defers when trying to send with an uploaded attachment that has an invalid downloadUrl") { var didDefer: Bool = false @@ -356,7 +346,7 @@ class MessageSendJobSpec: QuickSpec { expect(didDefer).to(beTrue()) } - // MARK: ------ inserts an attachment upload job before the message send job + // MARK: -------- inserts an attachment upload job before the message send job it("inserts an attachment upload job before the message send job") { mockJobRunner .when { @@ -397,7 +387,7 @@ class MessageSendJobSpec: QuickSpec { }) } - // MARK: ------ creates a dependency between the new job and the existing one + // MARK: -------- creates a dependency between the new job and the existing one it("creates a dependency between the new job and the existing one") { MessageSendJob.run( job, diff --git a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigContactsSpec.swift b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigContactsSpec.swift deleted file mode 100644 index a57839b28..000000000 --- a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigContactsSpec.swift +++ /dev/null @@ -1,547 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB -import Sodium -import SessionUtil -import SessionUtilitiesKit - -import Quick -import Nimble - -@testable import SessionMessagingKit - -/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches -class ConfigContactsSpec { - enum ContactProperty: CaseIterable { - case name - case nickname - case approved - case approved_me - case blocked - case profile_pic - case created - case notifications - case mute_until - } - - // MARK: - Spec - - static func spec() { - context("CONTACTS") { - // MARK: - when checking error catching - context("when checking error catching") { - var seed: Data! - var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)! - var edSK: [UInt8]! - var error: UnsafeMutablePointer? - var conf: UnsafeMutablePointer? - - beforeEach { - seed = Data(hex: "0123456789abcdef0123456789abcdef") - - // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately - identity = try! Identity.generate(from: seed) - edSK = identity.ed25519KeyPair.secretKey - - // Initialize a brand new, empty config because we have no dump data to deal with. - error = nil - conf = nil - _ = contacts_init(&conf, &edSK, nil, 0, error) - error?.deallocate() - } - - // MARK: -- it can catch size limit errors thrown when pushing - it("can catch size limit errors thrown when pushing") { - var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) - - try (0..<10000).forEach { index in - var contact: contacts_contact = try createContact( - for: index, - in: conf, - rand: &randomGenerator, - maxing: .allProperties - ) - contacts_set(conf, &contact) - } - - expect(contacts_size(conf)).to(equal(10000)) - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_dump(conf)).to(beTrue()) - - expect { - try CExceptionHelper.performSafely { config_push(conf).deallocate() } - } - .to(throwError(NSError(domain: "cpp_exception", code: -2, userInfo: ["NSLocalizedDescription": "Config data is too large"]))) - } - } - - // MARK: - when checking size limits - context("when checking size limits") { - var numRecords: Int! - var seed: Data! - var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)! - var edSK: [UInt8]! - var error: UnsafeMutablePointer? - var conf: UnsafeMutablePointer? - - beforeEach { - numRecords = 0 - seed = Data(hex: "0123456789abcdef0123456789abcdef") - - // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately - identity = try! Identity.generate(from: seed) - edSK = identity.ed25519KeyPair.secretKey - - // Initialize a brand new, empty config because we have no dump data to deal with. - error = nil - conf = nil - _ = contacts_init(&conf, &edSK, nil, 0, error) - error?.deallocate() - } - - // MARK: -- has not changed the max empty records - it("has not changed the max empty records") { - var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) - - for index in (0..<100000) { - var contact: contacts_contact = try createContact( - for: index, - in: conf, - rand: &randomGenerator - ) - contacts_set(conf, &contact) - - do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } - catch { break } - - // We successfully inserted a contact and didn't hit the limit so increment the counter - numRecords += 1 - } - - // Check that the record count matches the maximum when we last checked - expect(numRecords).to(equal(2370)) - } - - // MARK: -- has not changed the max name only records - it("has not changed the max name only records") { - var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) - - for index in (0..<100000) { - var contact: contacts_contact = try createContact( - for: index, - in: conf, - rand: &randomGenerator, - maxing: [.name] - ) - contacts_set(conf, &contact) - - do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } - catch { break } - - // We successfully inserted a contact and didn't hit the limit so increment the counter - numRecords += 1 - } - - // Check that the record count matches the maximum when we last checked - expect(numRecords).to(equal(796)) - } - - // MARK: -- has not changed the max name and profile pic only records - it("has not changed the max name and profile pic only records") { - var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) - - for index in (0..<100000) { - var contact: contacts_contact = try createContact( - for: index, - in: conf, - rand: &randomGenerator, - maxing: [.name, .profile_pic] - ) - contacts_set(conf, &contact) - - do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } - catch { break } - - // We successfully inserted a contact and didn't hit the limit so increment the counter - numRecords += 1 - } - - // Check that the record count matches the maximum when we last checked - expect(numRecords).to(equal(290)) - } - - // MARK: -- has not changed the max filled records - it("has not changed the max filled records") { - var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) - - for index in (0..<100000) { - var contact: contacts_contact = try createContact( - for: index, - in: conf, - rand: &randomGenerator, - maxing: .allProperties - ) - contacts_set(conf, &contact) - - do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } - catch { break } - - // We successfully inserted a contact and didn't hit the limit so increment the counter - numRecords += 1 - } - - // Check that the record count matches the maximum when we last checked - expect(numRecords).to(equal(236)) - } - } - - // MARK: - generates config correctly - - it("generates config correctly") { - let createdTs: Int64 = 1680064059 - let nowTs: Int64 = Int64(Date().timeIntervalSince1970) - let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") - - // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately - let identity = try! Identity.generate(from: seed) - var edSK: [UInt8] = identity.ed25519KeyPair.secretKey - expect(edSK.toHexString().suffix(64)) - .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) - expect(identity.x25519KeyPair.publicKey.toHexString()) - .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) - expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) - - // Initialize a brand new, empty config because we have no dump data to deal with. - let error: UnsafeMutablePointer? = nil - var conf: UnsafeMutablePointer? = nil - expect(contacts_init(&conf, &edSK, nil, 0, error)).to(equal(0)) - error?.deallocate() - - // Empty contacts shouldn't have an existing contact - let definitelyRealId: String = "050000000000000000000000000000000000000000000000000000000000000000" - var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() - let contactPtr: UnsafeMutablePointer? = nil - expect(contacts_get(conf, contactPtr, &cDefinitelyRealId)).to(beFalse()) - - expect(contacts_size(conf)).to(equal(0)) - - var contact2: contacts_contact = contacts_contact() - expect(contacts_get_or_construct(conf, &contact2, &cDefinitelyRealId)).to(beTrue()) - expect(String(libSessionVal: contact2.name)).to(beEmpty()) - expect(String(libSessionVal: contact2.nickname)).to(beEmpty()) - expect(contact2.approved).to(beFalse()) - expect(contact2.approved_me).to(beFalse()) - expect(contact2.blocked).to(beFalse()) - expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently - expect(String(libSessionVal: contact2.profile_pic.url)).to(beEmpty()) - expect(contact2.created).to(equal(0)) - expect(contact2.notifications).to(equal(CONVO_NOTIFY_DEFAULT)) - expect(contact2.mute_until).to(equal(0)) - - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_dump(conf)).to(beFalse()) - - let pushData1: UnsafeMutablePointer = config_push(conf) - expect(pushData1.pointee.seqno).to(equal(0)) - pushData1.deallocate() - - // Update the contact data - contact2.name = "Joe".toLibSession() - contact2.nickname = "Joey".toLibSession() - contact2.approved = true - contact2.approved_me = true - contact2.created = createdTs - contact2.notifications = CONVO_NOTIFY_ALL - contact2.mute_until = nowTs + 1800 - - // Update the contact - contacts_set(conf, &contact2) - - // Ensure the contact details were updated - var contact3: contacts_contact = contacts_contact() - expect(contacts_get(conf, &contact3, &cDefinitelyRealId)).to(beTrue()) - expect(String(libSessionVal: contact3.name)).to(equal("Joe")) - expect(String(libSessionVal: contact3.nickname)).to(equal("Joey")) - expect(contact3.approved).to(beTrue()) - expect(contact3.approved_me).to(beTrue()) - expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently - expect(String(libSessionVal: contact3.profile_pic.url)).to(beEmpty()) - expect(contact3.blocked).to(beFalse()) - expect(String(libSessionVal: contact3.session_id)).to(equal(definitelyRealId)) - expect(contact3.created).to(equal(createdTs)) - expect(contact2.notifications).to(equal(CONVO_NOTIFY_ALL)) - expect(contact2.mute_until).to(equal(nowTs + 1800)) - - - // Since we've made changes, we should need to push new config to the swarm, *and* should need - // to dump the updated state: - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_dump(conf)).to(beTrue()) - - // incremented since we made changes (this only increments once between - // dumps; even though we changed multiple fields here). - let pushData2: UnsafeMutablePointer = config_push(conf) - - // incremented since we made changes (this only increments once between - // dumps; even though we changed multiple fields here). - expect(pushData2.pointee.seqno).to(equal(1)) - - // Pretend we uploaded it - let fakeHash1: String = "fakehash1" - var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() - config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_dump(conf)).to(beTrue()) - pushData2.deallocate() - - // NB: Not going to check encrypted data and decryption here because that's general (not - // specific to contacts) and is covered already in the user profile tests. - var dump1: UnsafeMutablePointer? = nil - var dump1Len: Int = 0 - config_dump(conf, &dump1, &dump1Len) - - let error2: UnsafeMutablePointer? = nil - var conf2: UnsafeMutablePointer? = nil - expect(contacts_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0)) - error2?.deallocate() - dump1?.deallocate() - - expect(config_needs_push(conf2)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - - let pushData3: UnsafeMutablePointer = config_push(conf2) - expect(pushData3.pointee.seqno).to(equal(1)) - pushData3.deallocate() - - // Because we just called dump() above, to load up contacts2 - expect(config_needs_dump(conf)).to(beFalse()) - - // Ensure the contact details were updated - var contact4: contacts_contact = contacts_contact() - expect(contacts_get(conf2, &contact4, &cDefinitelyRealId)).to(beTrue()) - expect(String(libSessionVal: contact4.name)).to(equal("Joe")) - expect(String(libSessionVal: contact4.nickname)).to(equal("Joey")) - expect(contact4.approved).to(beTrue()) - expect(contact4.approved_me).to(beTrue()) - expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently - expect(String(libSessionVal: contact4.profile_pic.url)).to(beEmpty()) - expect(contact4.blocked).to(beFalse()) - expect(contact4.created).to(equal(createdTs)) - - let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111" - var cAnotherId: [CChar] = anotherId.cArray.nullTerminated() - var contact5: contacts_contact = contacts_contact() - expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue()) - expect(String(libSessionVal: contact5.name)).to(beEmpty()) - expect(String(libSessionVal: contact5.nickname)).to(beEmpty()) - expect(contact5.approved).to(beFalse()) - expect(contact5.approved_me).to(beFalse()) - expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently - expect(String(libSessionVal: contact5.profile_pic.url)).to(beEmpty()) - expect(contact5.blocked).to(beFalse()) - - // We're not setting any fields, but we should still keep a record of the session id - contacts_set(conf2, &contact5) - expect(config_needs_push(conf2)).to(beTrue()) - - let pushData4: UnsafeMutablePointer = config_push(conf2) - expect(pushData4.pointee.seqno).to(equal(2)) - - // Check the merging - let fakeHash2: String = "fakehash2" - var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() - var mergeHashes: [UnsafePointer?] = [cFakeHash2].unsafeCopy() - var mergeData: [UnsafePointer?] = [UnsafePointer(pushData4.pointee.config)] - var mergeSize: [Int] = [pushData4.pointee.config_len] - expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1)) - config_confirm_pushed(conf2, pushData4.pointee.seqno, &cFakeHash2) - mergeHashes.forEach { $0?.deallocate() } - pushData4.deallocate() - - expect(config_needs_push(conf)).to(beFalse()) - - let pushData5: UnsafeMutablePointer = config_push(conf) - expect(pushData5.pointee.seqno).to(equal(2)) - pushData5.deallocate() - - // Iterate through and make sure we got everything we expected - var sessionIds: [String] = [] - var nicknames: [String] = [] - expect(contacts_size(conf)).to(equal(2)) - - var contact6: contacts_contact = contacts_contact() - let contactIterator: UnsafeMutablePointer = contacts_iterator_new(conf) - while !contacts_iterator_done(contactIterator, &contact6) { - sessionIds.append(String(libSessionVal: contact6.session_id)) - nicknames.append(String(libSessionVal: contact6.nickname, nullIfEmpty: true) ?? "(N/A)") - contacts_iterator_advance(contactIterator) - } - contacts_iterator_free(contactIterator) // Need to free the iterator - - expect(sessionIds.count).to(equal(2)) - expect(sessionIds.count).to(equal(contacts_size(conf))) - expect(sessionIds.first).to(equal(definitelyRealId)) - expect(sessionIds.last).to(equal(anotherId)) - expect(nicknames.first).to(equal("Joey")) - expect(nicknames.last).to(equal("(N/A)")) - - // Conflict! Oh no! - - // On client 1 delete a contact: - contacts_erase(conf, definitelyRealId) - - // Client 2 adds a new friend: - let thirdId: String = "052222222222222222222222222222222222222222222222222222222222222222" - var cThirdId: [CChar] = thirdId.cArray.nullTerminated() - var contact7: contacts_contact = contacts_contact() - expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue()) - contact7.nickname = "Nickname 3".toLibSession() - contact7.approved = true - contact7.approved_me = true - contact7.profile_pic.url = "http://example.com/huge.bmp".toLibSession() - contact7.profile_pic.key = "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession() - contacts_set(conf2, &contact7) - - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_push(conf2)).to(beTrue()) - - let pushData6: UnsafeMutablePointer = config_push(conf) - expect(pushData6.pointee.seqno).to(equal(3)) - - let pushData7: UnsafeMutablePointer = config_push(conf2) - expect(pushData7.pointee.seqno).to(equal(3)) - - let pushData6Str: String = String(pointer: pushData6.pointee.config, length: pushData6.pointee.config_len, encoding: .ascii)! - let pushData7Str: String = String(pointer: pushData7.pointee.config, length: pushData7.pointee.config_len, encoding: .ascii)! - expect(pushData6Str).toNot(equal(pushData7Str)) - expect([String](pointer: pushData6.pointee.obsolete, count: pushData6.pointee.obsolete_len)) - .to(equal([fakeHash2])) - expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len)) - .to(equal([fakeHash2])) - - let fakeHash3a: String = "fakehash3a" - var cFakeHash3a: [CChar] = fakeHash3a.cArray.nullTerminated() - let fakeHash3b: String = "fakehash3b" - var cFakeHash3b: [CChar] = fakeHash3b.cArray.nullTerminated() - config_confirm_pushed(conf, pushData6.pointee.seqno, &cFakeHash3a) - config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash3b) - - var mergeHashes2: [UnsafePointer?] = [cFakeHash3b].unsafeCopy() - var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData7.pointee.config)] - var mergeSize2: [Int] = [pushData7.pointee.config_len] - expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1)) - expect(config_needs_push(conf)).to(beTrue()) - - var mergeHashes3: [UnsafePointer?] = [cFakeHash3a].unsafeCopy() - var mergeData3: [UnsafePointer?] = [UnsafePointer(pushData6.pointee.config)] - var mergeSize3: [Int] = [pushData6.pointee.config_len] - expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1)) - expect(config_needs_push(conf2)).to(beTrue()) - mergeHashes2.forEach { $0?.deallocate() } - mergeHashes3.forEach { $0?.deallocate() } - pushData6.deallocate() - pushData7.deallocate() - - let pushData8: UnsafeMutablePointer = config_push(conf) - expect(pushData8.pointee.seqno).to(equal(4)) - - let pushData9: UnsafeMutablePointer = config_push(conf2) - expect(pushData9.pointee.seqno).to(equal(pushData8.pointee.seqno)) - - let pushData8Str: String = String(pointer: pushData8.pointee.config, length: pushData8.pointee.config_len, encoding: .ascii)! - let pushData9Str: String = String(pointer: pushData9.pointee.config, length: pushData9.pointee.config_len, encoding: .ascii)! - expect(pushData8Str).to(equal(pushData9Str)) - expect([String](pointer: pushData8.pointee.obsolete, count: pushData8.pointee.obsolete_len)) - .to(equal([fakeHash3b, fakeHash3a])) - expect([String](pointer: pushData9.pointee.obsolete, count: pushData9.pointee.obsolete_len)) - .to(equal([fakeHash3a, fakeHash3b])) - - let fakeHash4: String = "fakeHash4" - var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() - config_confirm_pushed(conf, pushData8.pointee.seqno, &cFakeHash4) - config_confirm_pushed(conf2, pushData9.pointee.seqno, &cFakeHash4) - pushData8.deallocate() - pushData9.deallocate() - - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_push(conf2)).to(beFalse()) - - // Validate the changes - var sessionIds2: [String] = [] - var nicknames2: [String] = [] - expect(contacts_size(conf)).to(equal(2)) - - var contact8: contacts_contact = contacts_contact() - let contactIterator2: UnsafeMutablePointer = contacts_iterator_new(conf) - while !contacts_iterator_done(contactIterator2, &contact8) { - sessionIds2.append(String(libSessionVal: contact8.session_id)) - nicknames2.append(String(libSessionVal: contact8.nickname, nullIfEmpty: true) ?? "(N/A)") - contacts_iterator_advance(contactIterator2) - } - contacts_iterator_free(contactIterator2) // Need to free the iterator - - expect(sessionIds2.count).to(equal(2)) - expect(sessionIds2.first).to(equal(anotherId)) - expect(sessionIds2.last).to(equal(thirdId)) - expect(nicknames2.first).to(equal("(N/A)")) - expect(nicknames2.last).to(equal("Nickname 3")) - } - } - } - - // MARK: - Convenience - - private static func createContact( - for index: Int, - in conf: UnsafeMutablePointer?, - rand: inout ARC4RandomNumberGenerator, - maxing properties: [ContactProperty] = [] - ) throws -> contacts_contact { - let postPrefixId: String = "05\(rand.nextBytes(count: 32).toHexString())" - let sessionId: String = ("05\(index)a" + postPrefixId.suffix(postPrefixId.count - "05\(index)a".count)) - var cSessionId: [CChar] = sessionId.cArray.nullTerminated() - var contact: contacts_contact = contacts_contact() - - guard contacts_get_or_construct(conf, &contact, &cSessionId) else { - throw SessionUtilError.getOrConstructFailedUnexpectedly - } - - // Set the values to the maximum data that can fit - properties.forEach { property in - switch property { - case .approved: contact.approved = true - case .approved_me: contact.approved_me = true - case .blocked: contact.blocked = true - case .created: contact.created = Int64.max - case .notifications: contact.notifications = CONVO_NOTIFY_MENTIONS_ONLY - case .mute_until: contact.mute_until = Int64.max - - case .name: - contact.name = rand.nextBytes(count: SessionUtil.libSessionMaxNameByteLength) - .toHexString() - .toLibSession() - - case .nickname: - contact.nickname = rand.nextBytes(count: SessionUtil.libSessionMaxNameByteLength) - .toHexString() - .toLibSession() - - case .profile_pic: - contact.profile_pic = user_profile_pic( - url: rand.nextBytes(count: SessionUtil.libSessionMaxProfileUrlByteLength) - .toHexString() - .toLibSession(), - key: Data(rand.nextBytes(count: 32)) - .toLibSession() - ) - } - } - - return contact - } -} - -fileprivate extension Array where Element == ConfigContactsSpec.ContactProperty { - static var allProperties: [ConfigContactsSpec.ContactProperty] = ConfigContactsSpec.ContactProperty.allCases -} diff --git a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigConvoInfoVolatileSpec.swift b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigConvoInfoVolatileSpec.swift deleted file mode 100644 index 86325c4ad..000000000 --- a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigConvoInfoVolatileSpec.swift +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import Sodium -import SessionUtil -import SessionUtilitiesKit - -import Quick -import Nimble - -/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches -class ConfigConvoInfoVolatileSpec { - // MARK: - Spec - - static func spec() { - context("CONVO_INFO_VOLATILE") { - it("generates config correctly") { - let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") - - // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately - let identity = try! Identity.generate(from: seed) - var edSK: [UInt8] = identity.ed25519KeyPair.secretKey - expect(edSK.toHexString().suffix(64)) - .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) - expect(identity.x25519KeyPair.publicKey.toHexString()) - .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) - expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) - - // Initialize a brand new, empty config because we have no dump data to deal with. - let error: UnsafeMutablePointer? = nil - var conf: UnsafeMutablePointer? = nil - expect(convo_info_volatile_init(&conf, &edSK, nil, 0, error)).to(equal(0)) - error?.deallocate() - - // Empty contacts shouldn't have an existing contact - let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000" - var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() - var oneToOne1: convo_info_volatile_1to1 = convo_info_volatile_1to1() - expect(convo_info_volatile_get_1to1(conf, &oneToOne1, &cDefinitelyRealId)).to(beFalse()) - expect(convo_info_volatile_size(conf)).to(equal(0)) - - var oneToOne2: convo_info_volatile_1to1 = convo_info_volatile_1to1() - expect(convo_info_volatile_get_or_construct_1to1(conf, &oneToOne2, &cDefinitelyRealId)) - .to(beTrue()) - expect(String(libSessionVal: oneToOne2.session_id)).to(equal(definitelyRealId)) - expect(oneToOne2.last_read).to(equal(0)) - expect(oneToOne2.unread).to(beFalse()) - - // No need to sync a conversation with a default state - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_dump(conf)).to(beFalse()) - - // Update the last read - let nowTimestampMs: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000)) - oneToOne2.last_read = nowTimestampMs - - // The new data doesn't get stored until we call this: - convo_info_volatile_set_1to1(conf, &oneToOne2) - - var legacyGroup1: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() - var oneToOne3: convo_info_volatile_1to1 = convo_info_volatile_1to1() - expect(convo_info_volatile_get_legacy_group(conf, &legacyGroup1, &cDefinitelyRealId)) - .to(beFalse()) - expect(convo_info_volatile_get_1to1(conf, &oneToOne3, &cDefinitelyRealId)).to(beTrue()) - expect(oneToOne3.last_read).to(equal(nowTimestampMs)) - - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_dump(conf)).to(beTrue()) - - let openGroupBaseUrl: String = "http://Example.ORG:5678" - var cOpenGroupBaseUrl: [CChar] = openGroupBaseUrl.cArray.nullTerminated() - let openGroupBaseUrlResult: String = openGroupBaseUrl.lowercased() - // ("http://Example.ORG:5678" - // .lowercased() - // .cArray + - // [CChar](repeating: 0, count: (268 - openGroupBaseUrl.count)) - // ) - let openGroupRoom: String = "SudokuRoom" - var cOpenGroupRoom: [CChar] = openGroupRoom.cArray.nullTerminated() - let openGroupRoomResult: String = openGroupRoom.lowercased() - // ("SudokuRoom" - // .lowercased() - // .cArray + - // [CChar](repeating: 0, count: (65 - openGroupRoom.count)) - // ) - var cOpenGroupPubkey: [UInt8] = Data(hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - .bytes - var community1: convo_info_volatile_community = convo_info_volatile_community() - expect(convo_info_volatile_get_or_construct_community(conf, &community1, &cOpenGroupBaseUrl, &cOpenGroupRoom, &cOpenGroupPubkey)).to(beTrue()) - expect(String(libSessionVal: community1.base_url)).to(equal(openGroupBaseUrlResult)) - expect(String(libSessionVal: community1.room)).to(equal(openGroupRoomResult)) - expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString()) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - community1.unread = true - - // The new data doesn't get stored until we call this: - convo_info_volatile_set_community(conf, &community1); - - // We don't need to push since we haven't changed anything, so this call is mainly just for - // testing: - let pushData1: UnsafeMutablePointer = config_push(conf) - expect(pushData1.pointee.seqno).to(equal(1)) - - // Pretend we uploaded it - let fakeHash1: String = "fakehash1" - var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() - config_confirm_pushed(conf, pushData1.pointee.seqno, &cFakeHash1) - expect(config_needs_dump(conf)).to(beTrue()) - expect(config_needs_push(conf)).to(beFalse()) - pushData1.deallocate() - - var dump1: UnsafeMutablePointer? = nil - var dump1Len: Int = 0 - config_dump(conf, &dump1, &dump1Len) - - let error2: UnsafeMutablePointer? = nil - var conf2: UnsafeMutablePointer? = nil - expect(convo_info_volatile_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0)) - error2?.deallocate() - dump1?.deallocate() - - expect(config_needs_dump(conf2)).to(beFalse()) - expect(config_needs_push(conf2)).to(beFalse()) - - var oneToOne4: convo_info_volatile_1to1 = convo_info_volatile_1to1() - expect(convo_info_volatile_get_1to1(conf2, &oneToOne4, &cDefinitelyRealId)).to(equal(true)) - expect(oneToOne4.last_read).to(equal(nowTimestampMs)) - expect(String(libSessionVal: oneToOne4.session_id)).to(equal(definitelyRealId)) - expect(oneToOne4.unread).to(beFalse()) - - var community2: convo_info_volatile_community = convo_info_volatile_community() - expect(convo_info_volatile_get_community(conf2, &community2, &cOpenGroupBaseUrl, &cOpenGroupRoom)).to(beTrue()) - expect(String(libSessionVal: community2.base_url)).to(equal(openGroupBaseUrlResult)) - expect(String(libSessionVal: community2.room)).to(equal(openGroupRoomResult)) - expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString()) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - community2.unread = true - - let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111" - var cAnotherId: [CChar] = anotherId.cArray.nullTerminated() - var oneToOne5: convo_info_volatile_1to1 = convo_info_volatile_1to1() - expect(convo_info_volatile_get_or_construct_1to1(conf2, &oneToOne5, &cAnotherId)).to(beTrue()) - oneToOne5.unread = true - convo_info_volatile_set_1to1(conf2, &oneToOne5) - - let thirdId: String = "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - var cThirdId: [CChar] = thirdId.cArray.nullTerminated() - var legacyGroup2: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() - expect(convo_info_volatile_get_or_construct_legacy_group(conf2, &legacyGroup2, &cThirdId)).to(beTrue()) - legacyGroup2.last_read = (nowTimestampMs - 50) - convo_info_volatile_set_legacy_group(conf2, &legacyGroup2) - expect(config_needs_push(conf2)).to(beTrue()) - - let pushData2: UnsafeMutablePointer = config_push(conf2) - expect(pushData2.pointee.seqno).to(equal(2)) - - // Check the merging - let fakeHash2: String = "fakehash2" - var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() - var mergeHashes: [UnsafePointer?] = [cFakeHash2].unsafeCopy() - var mergeData: [UnsafePointer?] = [UnsafePointer(pushData2.pointee.config)] - var mergeSize: [Int] = [pushData2.pointee.config_len] - expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1)) - config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash2) - pushData2.deallocate() - - expect(config_needs_push(conf)).to(beFalse()) - - for targetConf in [conf, conf2] { - // Iterate through and make sure we got everything we expected - var seen: [String] = [] - expect(convo_info_volatile_size(conf)).to(equal(4)) - expect(convo_info_volatile_size_1to1(conf)).to(equal(2)) - expect(convo_info_volatile_size_communities(conf)).to(equal(1)) - expect(convo_info_volatile_size_legacy_groups(conf)).to(equal(1)) - - var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1() - var c2: convo_info_volatile_community = convo_info_volatile_community() - var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() - let it: OpaquePointer = convo_info_volatile_iterator_new(targetConf) - - while !convo_info_volatile_iterator_done(it) { - if convo_info_volatile_it_is_1to1(it, &c1) { - seen.append("1-to-1: \(String(libSessionVal: c1.session_id))") - } - else if convo_info_volatile_it_is_community(it, &c2) { - seen.append("og: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") - } - else if convo_info_volatile_it_is_legacy_group(it, &c3) { - seen.append("cl: \(String(libSessionVal: c3.group_id))") - } - - convo_info_volatile_iterator_advance(it) - } - - convo_info_volatile_iterator_free(it) - - expect(seen).to(equal([ - "1-to-1: 051111111111111111111111111111111111111111111111111111111111111111", - "1-to-1: 055000000000000000000000000000000000000000000000000000000000000000", - "og: http://example.org:5678/r/sudokuroom", - "cl: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - ])) - } - - let fourthId: String = "052000000000000000000000000000000000000000000000000000000000000000" - var cFourthId: [CChar] = fourthId.cArray.nullTerminated() - expect(config_needs_push(conf)).to(beFalse()) - convo_info_volatile_erase_1to1(conf, &cFourthId) - expect(config_needs_push(conf)).to(beFalse()) - convo_info_volatile_erase_1to1(conf, &cDefinitelyRealId) - expect(config_needs_push(conf)).to(beTrue()) - expect(convo_info_volatile_size(conf)).to(equal(3)) - expect(convo_info_volatile_size_1to1(conf)).to(equal(1)) - - // Check the single-type iterators: - var seen1: [String?] = [] - var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1() - let it1: OpaquePointer = convo_info_volatile_iterator_new_1to1(conf) - - while !convo_info_volatile_iterator_done(it1) { - expect(convo_info_volatile_it_is_1to1(it1, &c1)).to(beTrue()) - - seen1.append(String(libSessionVal: c1.session_id)) - convo_info_volatile_iterator_advance(it1) - } - - convo_info_volatile_iterator_free(it1) - expect(seen1).to(equal([ - "051111111111111111111111111111111111111111111111111111111111111111" - ])) - - var seen2: [String?] = [] - var c2: convo_info_volatile_community = convo_info_volatile_community() - let it2: OpaquePointer = convo_info_volatile_iterator_new_communities(conf) - - while !convo_info_volatile_iterator_done(it2) { - expect(convo_info_volatile_it_is_community(it2, &c2)).to(beTrue()) - - seen2.append(String(libSessionVal: c2.base_url)) - convo_info_volatile_iterator_advance(it2) - } - - convo_info_volatile_iterator_free(it2) - expect(seen2).to(equal([ - "http://example.org:5678" - ])) - - var seen3: [String?] = [] - var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() - let it3: OpaquePointer = convo_info_volatile_iterator_new_legacy_groups(conf) - - while !convo_info_volatile_iterator_done(it3) { - expect(convo_info_volatile_it_is_legacy_group(it3, &c3)).to(beTrue()) - - seen3.append(String(libSessionVal: c3.group_id)) - convo_info_volatile_iterator_advance(it3) - } - - convo_info_volatile_iterator_free(it3) - expect(seen3).to(equal([ - "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - ])) - } - } - } -} diff --git a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserGroupsSpec.swift b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserGroupsSpec.swift deleted file mode 100644 index 926cf74f6..000000000 --- a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserGroupsSpec.swift +++ /dev/null @@ -1,589 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import Sodium -import SessionUtil -import SessionUtilitiesKit -import SessionMessagingKit - -import Quick -import Nimble - -/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches -class ConfigUserGroupsSpec { - // MARK: - Spec - - static func spec() { - it("parses community URLs correctly") { - let result1 = SessionUtil.parseCommunity(url: [ - "https://example.com/", - "SomeRoom?public_key=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - ].joined()) - let result2 = SessionUtil.parseCommunity(url: [ - "HTTPS://EXAMPLE.COM/", - "sOMErOOM?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" - ].joined()) - let result3 = SessionUtil.parseCommunity(url: [ - "HTTPS://EXAMPLE.COM/r/", - "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" - ].joined()) - let result4 = SessionUtil.parseCommunity(url: [ - "http://example.com/r/", - "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" - ].joined()) - let result5 = SessionUtil.parseCommunity(url: [ - "HTTPS://EXAMPLE.com:443/r/", - "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" - ].joined()) - let result6 = SessionUtil.parseCommunity(url: [ - "HTTP://EXAMPLE.com:80/r/", - "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" - ].joined()) - let result7 = SessionUtil.parseCommunity(url: [ - "http://example.com:80/r/", - "someroom?public_key=ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8" - ].joined()) - let result8 = SessionUtil.parseCommunity(url: [ - "http://example.com:80/r/", - "someroom?public_key=yrtwk3hjixg66yjdeiuauk6p7hy1gtm8tgih55abrpnsxnpm3zzo" - ].joined()) - - expect(result1?.server).to(equal("https://example.com")) - expect(result1?.server).to(equal(result2?.server)) - expect(result1?.server).to(equal(result3?.server)) - expect(result1?.server).toNot(equal(result4?.server)) - expect(result4?.server).to(equal("http://example.com")) - expect(result1?.server).to(equal(result5?.server)) - expect(result4?.server).to(equal(result6?.server)) - expect(result4?.server).to(equal(result7?.server)) - expect(result4?.server).to(equal(result8?.server)) - expect(result1?.room).to(equal("SomeRoom")) - expect(result2?.room).to(equal("sOMErOOM")) - expect(result3?.room).to(equal("someroom")) - expect(result4?.room).to(equal("someroom")) - expect(result5?.room).to(equal("someroom")) - expect(result6?.room).to(equal("someroom")) - expect(result7?.room).to(equal("someroom")) - expect(result8?.room).to(equal("someroom")) - expect(result1?.publicKey) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - expect(result2?.publicKey) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - expect(result3?.publicKey) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - expect(result4?.publicKey) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - expect(result5?.publicKey) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - expect(result6?.publicKey) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - expect(result7?.publicKey) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - expect(result8?.publicKey) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - } - - context("USER_GROUPS") { - it("generates config correctly") { - let createdTs: Int64 = 1680064059 - let nowTs: Int64 = Int64(Date().timeIntervalSince1970) - let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") - - // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately - let identity = try! Identity.generate(from: seed) - var edSK: [UInt8] = identity.ed25519KeyPair.secretKey - expect(edSK.toHexString().suffix(64)) - .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) - expect(identity.x25519KeyPair.publicKey.toHexString()) - .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) - expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) - - // Initialize a brand new, empty config because we have no dump data to deal with. - let error: UnsafeMutablePointer? = nil - var conf: UnsafeMutablePointer? = nil - expect(user_groups_init(&conf, &edSK, nil, 0, error)).to(equal(0)) - error?.deallocate() - - // Empty contacts shouldn't have an existing contact - let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000" - var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() - let legacyGroup1: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cDefinitelyRealId) - expect(legacyGroup1?.pointee).to(beNil()) - expect(user_groups_size(conf)).to(equal(0)) - - let legacyGroup2: UnsafeMutablePointer = user_groups_get_or_construct_legacy_group(conf, &cDefinitelyRealId) - expect(legacyGroup2.pointee).toNot(beNil()) - expect(String(libSessionVal: legacyGroup2.pointee.session_id)) - .to(equal(definitelyRealId)) - expect(legacyGroup2.pointee.disappearing_timer).to(equal(0)) - expect(String(libSessionVal: legacyGroup2.pointee.enc_pubkey, fixedLength: 32)).to(equal("")) - expect(String(libSessionVal: legacyGroup2.pointee.enc_seckey, fixedLength: 32)).to(equal("")) - expect(legacyGroup2.pointee.priority).to(equal(0)) - expect(String(libSessionVal: legacyGroup2.pointee.name)).to(equal("")) - expect(legacyGroup2.pointee.joined_at).to(equal(0)) - expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_DEFAULT)) - expect(legacyGroup2.pointee.mute_until).to(equal(0)) - - // Iterate through and make sure we got everything we expected - var membersSeen1: [String: Bool] = [:] - var memberSessionId1: UnsafePointer? = nil - var memberAdmin1: Bool = false - let membersIt1: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2) - - while ugroups_legacy_members_next(membersIt1, &memberSessionId1, &memberAdmin1) { - membersSeen1[String(cString: memberSessionId1!)] = memberAdmin1 - } - - ugroups_legacy_members_free(membersIt1) - - expect(membersSeen1).to(beEmpty()) - - // No need to sync a conversation with a default state - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_dump(conf)).to(beFalse()) - - // We don't need to push since we haven't changed anything, so this call is mainly just for - // testing: - let pushData1: UnsafeMutablePointer = config_push(conf) - expect(pushData1.pointee.seqno).to(equal(0)) - expect([String](pointer: pushData1.pointee.obsolete, count: pushData1.pointee.obsolete_len)) - .to(beEmpty()) - expect(pushData1.pointee.config_len).to(equal(256)) - pushData1.deallocate() - - let users: [String] = [ - "050000000000000000000000000000000000000000000000000000000000000000", - "051111111111111111111111111111111111111111111111111111111111111111", - "052222222222222222222222222222222222222222222222222222222222222222", - "053333333333333333333333333333333333333333333333333333333333333333", - "054444444444444444444444444444444444444444444444444444444444444444", - "055555555555555555555555555555555555555555555555555555555555555555", - "056666666666666666666666666666666666666666666666666666666666666666" - ] - var cUsers: [[CChar]] = users.map { $0.cArray.nullTerminated() } - legacyGroup2.pointee.name = "Englishmen".toLibSession() - legacyGroup2.pointee.disappearing_timer = 60 - legacyGroup2.pointee.joined_at = createdTs - legacyGroup2.pointee.notifications = CONVO_NOTIFY_ALL - legacyGroup2.pointee.mute_until = (nowTs + 3600) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[0], false)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], true)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[4], true)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[5], false)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beFalse()) - - // Flip to and from admin - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], true)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], false)).to(beTrue()) - - expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[5])).to(beTrue()) - expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[4])).to(beTrue()) - - var membersSeen2: [String: Bool] = [:] - var memberSessionId2: UnsafePointer? = nil - var memberAdmin2: Bool = false - let membersIt2: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2) - - while ugroups_legacy_members_next(membersIt2, &memberSessionId2, &memberAdmin2) { - membersSeen2[String(cString: memberSessionId2!)] = memberAdmin2 - } - - ugroups_legacy_members_free(membersIt2) - - expect(membersSeen2).to(equal([ - "050000000000000000000000000000000000000000000000000000000000000000": false, - "051111111111111111111111111111111111111111111111111111111111111111": false, - "052222222222222222222222222222222222222222222222222222222222222222": true - ])) - - // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately - let groupSeed: Data = Data(hex: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff") - let groupEd25519KeyPair = Sodium().sign.keyPair(seed: groupSeed.bytes)! - let groupX25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: groupEd25519KeyPair.publicKey)! - - // Note: this isn't exactly what Session actually does here for legacy closed - // groups (rather it uses X25519 keys) but for this test the distinction doesn't matter. - legacyGroup2.pointee.enc_pubkey = Data(groupX25519PublicKey).toLibSession() - legacyGroup2.pointee.enc_seckey = Data(groupEd25519KeyPair.secretKey).toLibSession() - legacyGroup2.pointee.priority = 3 - - expect(Data(libSessionVal: legacyGroup2.pointee.enc_pubkey, count: 32).toHexString()) - .to(equal("c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e")) - expect(Data(libSessionVal: legacyGroup2.pointee.enc_seckey, count: 32).toHexString()) - .to(equal("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")) - - // The new data doesn't get stored until we call this: - user_groups_set_free_legacy_group(conf, legacyGroup2) - - let legacyGroup3: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cDefinitelyRealId) - expect(legacyGroup3?.pointee).toNot(beNil()) - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_dump(conf)).to(beTrue()) - ugroups_legacy_group_free(legacyGroup3) - - let communityPubkey: String = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - var cCommunityPubkey: [UInt8] = Data(hex: communityPubkey).cArray - var cCommunityBaseUrl: [CChar] = "http://Example.ORG:5678".cArray.nullTerminated() - var cCommunityRoom: [CChar] = "SudokuRoom".cArray.nullTerminated() - var community1: ugroups_community_info = ugroups_community_info() - expect(user_groups_get_or_construct_community(conf, &community1, &cCommunityBaseUrl, &cCommunityRoom, &cCommunityPubkey)) - .to(beTrue()) - - expect(String(libSessionVal: community1.base_url)).to(equal("http://example.org:5678")) // Note: lower-case - expect(String(libSessionVal: community1.room)).to(equal("SudokuRoom")) // Note: case-preserving - expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString()) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - community1.priority = 14 - - // The new data doesn't get stored until we call this: - user_groups_set_community(conf, &community1) - - // incremented since we made changes (this only increments once between - // dumps; even though we changed two fields here). - let pushData2: UnsafeMutablePointer = config_push(conf) - expect(pushData2.pointee.seqno).to(equal(1)) - expect([String](pointer: pushData2.pointee.obsolete, count: pushData2.pointee.obsolete_len)) - .to(beEmpty()) - - // Pretend we uploaded it - let fakeHash1: String = "fakehash1" - var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() - config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) - expect(config_needs_dump(conf)).to(beTrue()) - expect(config_needs_push(conf)).to(beFalse()) - - var dump1: UnsafeMutablePointer? = nil - var dump1Len: Int = 0 - config_dump(conf, &dump1, &dump1Len) - - let error2: UnsafeMutablePointer? = nil - var conf2: UnsafeMutablePointer? = nil - expect(user_groups_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0)) - error2?.deallocate() - dump1?.deallocate() - - expect(config_needs_dump(conf)).to(beFalse()) // Because we just called dump() above, to load up conf2 - expect(config_needs_push(conf)).to(beFalse()) - - let pushData3: UnsafeMutablePointer = config_push(conf) - expect(pushData3.pointee.seqno).to(equal(1)) - expect([String](pointer: pushData3.pointee.obsolete, count: pushData3.pointee.obsolete_len)) - .to(beEmpty()) - pushData3.deallocate() - - let currentHashes1: UnsafeMutablePointer? = config_current_hashes(conf) - expect([String](pointer: currentHashes1?.pointee.value, count: currentHashes1?.pointee.len)) - .to(equal(["fakehash1"])) - currentHashes1?.deallocate() - - expect(config_needs_push(conf2)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - - let pushData4: UnsafeMutablePointer = config_push(conf2) - expect(pushData4.pointee.seqno).to(equal(1)) - expect(config_needs_dump(conf2)).to(beFalse()) - expect([String](pointer: pushData4.pointee.obsolete, count: pushData4.pointee.obsolete_len)) - .to(beEmpty()) - pushData4.deallocate() - - let currentHashes2: UnsafeMutablePointer? = config_current_hashes(conf2) - expect([String](pointer: currentHashes2?.pointee.value, count: currentHashes2?.pointee.len)) - .to(equal(["fakehash1"])) - currentHashes2?.deallocate() - - expect(user_groups_size(conf2)).to(equal(2)) - expect(user_groups_size_communities(conf2)).to(equal(1)) - expect(user_groups_size_legacy_groups(conf2)).to(equal(1)) - - let legacyGroup4: UnsafeMutablePointer? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId) - expect(legacyGroup4?.pointee).toNot(beNil()) - expect(String(libSessionVal: legacyGroup4?.pointee.enc_pubkey, fixedLength: 32)).to(equal("")) - expect(String(libSessionVal: legacyGroup4?.pointee.enc_seckey, fixedLength: 32)).to(equal("")) - expect(legacyGroup4?.pointee.disappearing_timer).to(equal(60)) - expect(String(libSessionVal: legacyGroup4?.pointee.session_id)).to(equal(definitelyRealId)) - expect(legacyGroup4?.pointee.priority).to(equal(3)) - expect(String(libSessionVal: legacyGroup4?.pointee.name)).to(equal("Englishmen")) - expect(legacyGroup4?.pointee.joined_at).to(equal(createdTs)) - expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_ALL)) - expect(legacyGroup2.pointee.mute_until).to(equal(nowTs + 3600)) - - var membersSeen3: [String: Bool] = [:] - var memberSessionId3: UnsafePointer? = nil - var memberAdmin3: Bool = false - let membersIt3: OpaquePointer = ugroups_legacy_members_begin(legacyGroup4) - - while ugroups_legacy_members_next(membersIt3, &memberSessionId3, &memberAdmin3) { - membersSeen3[String(cString: memberSessionId3!)] = memberAdmin3 - } - - ugroups_legacy_members_free(membersIt3) - ugroups_legacy_group_free(legacyGroup4) - - expect(membersSeen3).to(equal([ - "050000000000000000000000000000000000000000000000000000000000000000": false, - "051111111111111111111111111111111111111111111111111111111111111111": false, - "052222222222222222222222222222222222222222222222222222222222222222": true - ])) - - expect(config_needs_push(conf2)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - - let pushData5: UnsafeMutablePointer = config_push(conf2) - expect(pushData5.pointee.seqno).to(equal(1)) - expect(config_needs_dump(conf2)).to(beFalse()) - pushData5.deallocate() - - for targetConf in [conf, conf2] { - // Iterate through and make sure we got everything we expected - var seen: [String] = [] - - var c1: ugroups_legacy_group_info = ugroups_legacy_group_info() - var c2: ugroups_community_info = ugroups_community_info() - let it: OpaquePointer = user_groups_iterator_new(targetConf) - - while !user_groups_iterator_done(it) { - if user_groups_it_is_legacy_group(it, &c1) { - var memberCount: Int = 0 - var adminCount: Int = 0 - ugroups_legacy_members_count(&c1, &memberCount, &adminCount) - seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members") - } - else if user_groups_it_is_community(it, &c2) { - seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") - } - else { - seen.append("unknown") - } - - user_groups_iterator_advance(it) - } - - user_groups_iterator_free(it) - - expect(seen).to(equal([ - "community: http://example.org:5678/r/SudokuRoom", - "legacy: Englishmen, 1 admins, 2 members" - ])) - } - - var cCommunity2BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated() - var cCommunity2Room: [CChar] = "sudokuRoom".cArray.nullTerminated() - var community2: ugroups_community_info = ugroups_community_info() - expect(user_groups_get_community(conf2, &community2, &cCommunity2BaseUrl, &cCommunity2Room)) - .to(beTrue()) - expect(String(libSessionVal: community2.base_url)).to(equal("http://example.org:5678")) - expect(String(libSessionVal: community2.room)).to(equal("SudokuRoom")) // Case preserved from the stored value, not the input value - expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString()) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - expect(community2.priority).to(equal(14)) - - expect(config_needs_push(conf2)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - - let pushData6: UnsafeMutablePointer = config_push(conf2) - expect(pushData6.pointee.seqno).to(equal(1)) - expect(config_needs_dump(conf2)).to(beFalse()) - pushData6.deallocate() - - community2.room = "sudokuRoom".toLibSession() // Change capitalization - user_groups_set_community(conf2, &community2) - - expect(config_needs_push(conf2)).to(beTrue()) - expect(config_needs_dump(conf2)).to(beTrue()) - - let fakeHash2: String = "fakehash2" - var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() - let pushData7: UnsafeMutablePointer = config_push(conf2) - expect(pushData7.pointee.seqno).to(equal(2)) - config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash2) - expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len)) - .to(equal([fakeHash1])) - - let currentHashes3: UnsafeMutablePointer? = config_current_hashes(conf2) - expect([String](pointer: currentHashes3?.pointee.value, count: currentHashes3?.pointee.len)) - .to(equal([fakeHash2])) - currentHashes3?.deallocate() - - var dump2: UnsafeMutablePointer? = nil - var dump2Len: Int = 0 - config_dump(conf2, &dump2, &dump2Len) - - expect(config_needs_push(conf2)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - - let pushData8: UnsafeMutablePointer = config_push(conf2) - expect(pushData8.pointee.seqno).to(equal(2)) - config_confirm_pushed(conf2, pushData8.pointee.seqno, &cFakeHash2) - expect(config_needs_dump(conf2)).to(beFalse()) - - var mergeHashes1: [UnsafePointer?] = [cFakeHash2].unsafeCopy() - var mergeData1: [UnsafePointer?] = [UnsafePointer(pushData8.pointee.config)] - var mergeSize1: [Int] = [pushData8.pointee.config_len] - expect(config_merge(conf, &mergeHashes1, &mergeData1, &mergeSize1, 1)).to(equal(1)) - pushData8.deallocate() - - var cCommunity3BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated() - var cCommunity3Room: [CChar] = "SudokuRoom".cArray.nullTerminated() - var community3: ugroups_community_info = ugroups_community_info() - expect(user_groups_get_community(conf, &community3, &cCommunity3BaseUrl, &cCommunity3Room)) - .to(beTrue()) - expect(String(libSessionVal: community3.room)).to(equal("sudokuRoom")) // We picked up the capitalization change - - expect(user_groups_size(conf)).to(equal(2)) - expect(user_groups_size_communities(conf)).to(equal(1)) - expect(user_groups_size_legacy_groups(conf)).to(equal(1)) - - let legacyGroup5: UnsafeMutablePointer? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId) - expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[4], false)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[5], true)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[6], true)).to(beTrue()) - expect(ugroups_legacy_member_remove(legacyGroup5, &cUsers[1])).to(beTrue()) - - expect(config_needs_push(conf2)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - - let pushData9: UnsafeMutablePointer = config_push(conf2) - expect(pushData9.pointee.seqno).to(equal(2)) - expect(config_needs_dump(conf2)).to(beFalse()) - pushData9.deallocate() - - user_groups_set_free_legacy_group(conf2, legacyGroup5) - expect(config_needs_push(conf2)).to(beTrue()) - expect(config_needs_dump(conf2)).to(beTrue()) - - var cCommunity4BaseUrl: [CChar] = "http://exAMple.ORG:5678".cArray.nullTerminated() - var cCommunity4Room: [CChar] = "sudokuROOM".cArray.nullTerminated() - user_groups_erase_community(conf2, &cCommunity4BaseUrl, &cCommunity4Room) - - let fakeHash3: String = "fakehash3" - var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated() - let pushData10: UnsafeMutablePointer = config_push(conf2) - config_confirm_pushed(conf2, pushData10.pointee.seqno, &cFakeHash3) - - expect(pushData10.pointee.seqno).to(equal(3)) - expect([String](pointer: pushData10.pointee.obsolete, count: pushData10.pointee.obsolete_len)) - .to(equal([fakeHash2])) - - let currentHashes4: UnsafeMutablePointer? = config_current_hashes(conf2) - expect([String](pointer: currentHashes4?.pointee.value, count: currentHashes4?.pointee.len)) - .to(equal([fakeHash3])) - currentHashes4?.deallocate() - - var mergeHashes2: [UnsafePointer?] = [cFakeHash3].unsafeCopy() - var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData10.pointee.config)] - var mergeSize2: [Int] = [pushData10.pointee.config_len] - expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1)) - - expect(user_groups_size(conf)).to(equal(1)) - expect(user_groups_size_communities(conf)).to(equal(0)) - expect(user_groups_size_legacy_groups(conf)).to(equal(1)) - - var prio: Int32 = 0 - var cBeanstalkBaseUrl: [CChar] = "http://jacksbeanstalk.org".cArray.nullTerminated() - var cBeanstalkPubkey: [UInt8] = Data( - hex: "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" - ).cArray - - ["fee", "fi", "fo", "fum"].forEach { room in - var cRoom: [CChar] = room.cArray.nullTerminated() - prio += 1 - - var community4: ugroups_community_info = ugroups_community_info() - expect(user_groups_get_or_construct_community(conf, &community4, &cBeanstalkBaseUrl, &cRoom, &cBeanstalkPubkey)) - .to(beTrue()) - community4.priority = prio - user_groups_set_community(conf, &community4) - } - - expect(user_groups_size(conf)).to(equal(5)) - expect(user_groups_size_communities(conf)).to(equal(4)) - expect(user_groups_size_legacy_groups(conf)).to(equal(1)) - - let fakeHash4: String = "fakehash4" - var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() - let pushData11: UnsafeMutablePointer = config_push(conf) - config_confirm_pushed(conf, pushData11.pointee.seqno, &cFakeHash4) - expect(pushData11.pointee.seqno).to(equal(4)) - expect([String](pointer: pushData11.pointee.obsolete, count: pushData11.pointee.obsolete_len)) - .to(equal([fakeHash3, fakeHash2, fakeHash1])) - - // Load some obsolete ones in just to check that they get immediately obsoleted - let fakeHash10: String = "fakehash10" - let cFakeHash10: [CChar] = fakeHash10.cArray.nullTerminated() - let fakeHash11: String = "fakehash11" - let cFakeHash11: [CChar] = fakeHash11.cArray.nullTerminated() - let fakeHash12: String = "fakehash12" - let cFakeHash12: [CChar] = fakeHash12.cArray.nullTerminated() - var mergeHashes3: [UnsafePointer?] = [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopy() - var mergeData3: [UnsafePointer?] = [ - UnsafePointer(pushData10.pointee.config), - UnsafePointer(pushData2.pointee.config), - UnsafePointer(pushData7.pointee.config), - UnsafePointer(pushData11.pointee.config) - ] - var mergeSize3: [Int] = [ - pushData10.pointee.config_len, - pushData2.pointee.config_len, - pushData7.pointee.config_len, - pushData11.pointee.config_len - ] - expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 4)).to(equal(4)) - expect(config_needs_dump(conf2)).to(beTrue()) - expect(config_needs_push(conf2)).to(beFalse()) - pushData2.deallocate() - pushData7.deallocate() - pushData10.deallocate() - pushData11.deallocate() - - let currentHashes5: UnsafeMutablePointer? = config_current_hashes(conf2) - expect([String](pointer: currentHashes5?.pointee.value, count: currentHashes5?.pointee.len)) - .to(equal([fakeHash4])) - currentHashes5?.deallocate() - - let pushData12: UnsafeMutablePointer = config_push(conf2) - expect(pushData12.pointee.seqno).to(equal(4)) - expect([String](pointer: pushData12.pointee.obsolete, count: pushData12.pointee.obsolete_len)) - .to(equal([fakeHash11, fakeHash12, fakeHash10, fakeHash3])) - pushData12.deallocate() - - for targetConf in [conf, conf2] { - // Iterate through and make sure we got everything we expected - var seen: [String] = [] - - var c1: ugroups_legacy_group_info = ugroups_legacy_group_info() - var c2: ugroups_community_info = ugroups_community_info() - let it: OpaquePointer = user_groups_iterator_new(targetConf) - - while !user_groups_iterator_done(it) { - if user_groups_it_is_legacy_group(it, &c1) { - var memberCount: Int = 0 - var adminCount: Int = 0 - ugroups_legacy_members_count(&c1, &memberCount, &adminCount) - - seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members") - } - else if user_groups_it_is_community(it, &c2) { - seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") - } - else { - seen.append("unknown") - } - - user_groups_iterator_advance(it) - } - - user_groups_iterator_free(it) - - expect(seen).to(equal([ - "community: http://jacksbeanstalk.org/r/fee", - "community: http://jacksbeanstalk.org/r/fi", - "community: http://jacksbeanstalk.org/r/fo", - "community: http://jacksbeanstalk.org/r/fum", - "legacy: Englishmen, 3 admins, 2 members" - ])) - } - } - } - } -} diff --git a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift deleted file mode 100644 index 849cdc85b..000000000 --- a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift +++ /dev/null @@ -1,414 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import Sodium -import SessionUtil -import SessionUtilitiesKit -import SessionMessagingKit - -import Quick -import Nimble - -/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches -class ConfigUserProfileSpec { - // MARK: - Spec - - static func spec() { - context("USER_PROFILE") { - it("generates config correctly") { - let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") - - // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately - let identity = try! Identity.generate(from: seed) - var edSK: [UInt8] = identity.ed25519KeyPair.secretKey - expect(edSK.toHexString().suffix(64)) - .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) - expect(identity.x25519KeyPair.publicKey.toHexString()) - .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) - expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) - - // Initialize a brand new, empty config because we have no dump data to deal with. - let error: UnsafeMutablePointer? = nil - var conf: UnsafeMutablePointer? = nil - expect(user_profile_init(&conf, &edSK, nil, 0, error)).to(equal(0)) - error?.deallocate() - - // We don't need to push anything, since this is an empty config - expect(config_needs_push(conf)).to(beFalse()) - // And we haven't changed anything so don't need to dump to db - expect(config_needs_dump(conf)).to(beFalse()) - - // Since it's empty there shouldn't be a name. - let namePtr: UnsafePointer? = user_profile_get_name(conf) - expect(namePtr).to(beNil()) - - // We don't need to push since we haven't changed anything, so this call is mainly just for - // testing: - let pushData1: UnsafeMutablePointer = config_push(conf) - expect(pushData1.pointee).toNot(beNil()) - expect(pushData1.pointee.seqno).to(equal(0)) - expect(pushData1.pointee.config_len).to(equal(256)) - - let encDomain: [CChar] = "UserProfile" - .bytes - .map { CChar(bitPattern: $0) } - expect(String(cString: config_encryption_domain(conf))).to(equal("UserProfile")) - - var toPushDecSize: Int = 0 - let toPushDecrypted: UnsafeMutablePointer? = config_decrypt(pushData1.pointee.config, pushData1.pointee.config_len, edSK, encDomain, &toPushDecSize) - let prefixPadding: String = (0..<193) - .map { _ in "\0" } - .joined() - expect(toPushDecrypted).toNot(beNil()) - expect(toPushDecSize).to(equal(216)) // 256 - 40 overhead - expect(String(pointer: toPushDecrypted, length: toPushDecSize)) - .to(equal("\(prefixPadding)d1:#i0e1:&de1:? = user_profile_get_name(conf) - expect(namePtr2).toNot(beNil()) - expect(String(cString: namePtr2!)).to(equal("Kallie")) - - let pic2: user_profile_pic = user_profile_get_pic(conf); - expect(String(libSessionVal: pic2.url)).to(equal("http://example.org/omg-pic-123.bmp")) - expect(Data(libSessionVal: pic2.key, count: ProfileManager.avatarAES256KeyByteLength)) - .to(equal("secret78901234567890123456789012".data(using: .utf8))) - expect(user_profile_get_nts_priority(conf)).to(equal(9)) - - // Since we've made changes, we should need to push new config to the swarm, *and* should need - // to dump the updated state: - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_dump(conf)).to(beTrue()) - - // incremented since we made changes (this only increments once between - // dumps; even though we changed two fields here). - let pushData2: UnsafeMutablePointer = config_push(conf) - expect(pushData2.pointee.seqno).to(equal(1)) - - // Note: This hex value differs from the value in the library tests because - // it looks like the library has an "end of cell mark" character added at the - // end (0x07 or '0007') so we need to manually add it to work - let expHash0: [UInt8] = Data(hex: "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965") - .bytes - // The data to be actually pushed, expanded like this to make it somewhat human-readable: - let expPush1Decrypted: [UInt8] = [""" - d - 1:#i1e - 1:& d - 1:+ i9e - 1:n 6:Kallie - 1:p 34:http://example.org/omg-pic-123.bmp - 1:q 32:secret78901234567890123456789012 - e - 1:< l - l i0e 32: - """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability - .bytes, - expHash0, - """ - de e - e - 1:= d - 1:+ 0: - 1:n 0: - 1:p 0: - 1:q 0: - e - e - """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability - .bytes - ].flatMap { $0 } - let expPush1Encrypted: [UInt8] = Data(hex: [ - "9693a69686da3055f1ecdfb239c3bf8e746951a36d888c2fb7c02e856a5c2091b24e39a7e1af828f", - "1fa09fe8bf7d274afde0a0847ba143c43ffb8722301b5ae32e2f078b9a5e19097403336e50b18c84", - "aade446cd2823b011f97d6ad2116a53feb814efecc086bc172d31f4214b4d7c630b63bbe575b0868", - "2d146da44915063a07a78556ab5eff4f67f6aa26211e8d330b53d28567a931028c393709a325425d", - "e7486ccde24416a7fd4a8ba5fa73899c65f4276dfaddd5b2100adcf0f793104fb235b31ce32ec656", - "056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea", - "49bf122762d7bc1d6d9c02f6d54f8384" - ].joined()).bytes - - let pushData2Str: String = String(pointer: pushData2.pointee.config, length: pushData2.pointee.config_len, encoding: .ascii)! - let expPush1EncryptedStr: String = String(pointer: expPush1Encrypted, length: expPush1Encrypted.count, encoding: .ascii)! - expect(pushData2Str).to(equal(expPush1EncryptedStr)) - - // Raw decryption doesn't unpad (i.e. the padding is part of the encrypted data) - var pushData2DecSize: Int = 0 - let pushData2Decrypted: UnsafeMutablePointer? = config_decrypt( - pushData2.pointee.config, - pushData2.pointee.config_len, - edSK, - encDomain, - &pushData2DecSize - ) - let prefixPadding2: String = (0..<(256 - 40 - expPush1Decrypted.count)) - .map { _ in "\0" } - .joined() - expect(pushData2DecSize).to(equal(216)) // 256 - 40 overhead - - let pushData2DecryptedStr: String = String(pointer: pushData2Decrypted, length: pushData2DecSize, encoding: .ascii)! - let expPush1DecryptedStr: String = String(pointer: expPush1Decrypted, length: expPush1Decrypted.count, encoding: .ascii) - .map { "\(prefixPadding2)\($0)" }! - expect(pushData2DecryptedStr).to(equal(expPush1DecryptedStr)) - pushData2Decrypted?.deallocate() - - // We haven't dumped, so still need to dump: - expect(config_needs_dump(conf)).to(beTrue()) - // We did call push, but we haven't confirmed it as stored yet, so this will still return true: - expect(config_needs_push(conf)).to(beTrue()) - - var dump1: UnsafeMutablePointer? = nil - var dump1Len: Int = 0 - - config_dump(conf, &dump1, &dump1Len) - // (in a real client we'd now store this to disk) - - expect(config_needs_dump(conf)).to(beFalse()) - - let expDump1: [CChar] = [ - """ - d - 1:! i2e - 1:$ \(expPush1Decrypted.count): - """ - .removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) - .bytes - .map { CChar(bitPattern: $0) }, - expPush1Decrypted - .map { CChar(bitPattern: $0) }, - """ - 1:(0: - 1:)le - e - """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) - .bytes - .map { CChar(bitPattern: $0) } - ].flatMap { $0 } - expect(String(pointer: dump1, length: dump1Len, encoding: .ascii)) - .to(equal(String(pointer: expDump1, length: expDump1.count, encoding: .ascii))) - dump1?.deallocate() - - // So now imagine we got back confirmation from the swarm that the push has been stored: - let fakeHash1: String = "fakehash1" - var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() - config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) - pushData2.deallocate() - - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_dump(conf)).to(beTrue()) // The confirmation changes state, so this makes us need a dump - - var dump2: UnsafeMutablePointer? = nil - var dump2Len: Int = 0 - config_dump(conf, &dump2, &dump2Len) - - let expDump2: [CChar] = [ - """ - d - 1:! i0e - 1:$ \(expPush1Decrypted.count): - """ - .removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) - .bytes - .map { CChar(bitPattern: $0) }, - expPush1Decrypted - .map { CChar(bitPattern: $0) }, - """ - 1:(9:fakehash1 - 1:)le - e - """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) - .bytes - .map { CChar(bitPattern: $0) } - ].flatMap { $0 } - expect(String(pointer: dump2, length: dump2Len, encoding: .ascii)) - .to(equal(String(pointer: expDump2, length: expDump2.count, encoding: .ascii))) - dump2?.deallocate() - expect(config_needs_dump(conf)).to(beFalse()) - - // Now we're going to set up a second, competing config object (in the real world this would be - // another Session client somewhere). - - // Start with an empty config, as above: - let error2: UnsafeMutablePointer? = nil - var conf2: UnsafeMutablePointer? = nil - expect(user_profile_init(&conf2, &edSK, nil, 0, error2)).to(equal(0)) - expect(config_needs_dump(conf2)).to(beFalse()) - error2?.deallocate() - - // Now imagine we just pulled down the `exp_push1` string from the swarm; we merge it into - // conf2: - var mergeHashes: [UnsafePointer?] = [cFakeHash1].unsafeCopy() - var mergeData: [UnsafePointer?] = [expPush1Encrypted].unsafeCopy() - var mergeSize: [Int] = [expPush1Encrypted.count] - expect(config_merge(conf2, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1)) - mergeHashes.forEach { $0?.deallocate() } - mergeData.forEach { $0?.deallocate() } - - // Our state has changed, so we need to dump: - expect(config_needs_dump(conf2)).to(beTrue()) - var dump3: UnsafeMutablePointer? = nil - var dump3Len: Int = 0 - config_dump(conf2, &dump3, &dump3Len) - // (store in db) - dump3?.deallocate() - expect(config_needs_dump(conf2)).to(beFalse()) - - // We *don't* need to push: even though we updated, all we did is update to the merged data (and - // didn't have any sort of merge conflict needed): - expect(config_needs_push(conf2)).to(beFalse()) - - // Now let's create a conflicting update: - - // Change the name on both clients: - user_profile_set_name(conf, "Nibbler") - user_profile_set_name(conf2, "Raz") - - // And, on conf2, we're also going to change the profile pic: - let p2: user_profile_pic = user_profile_pic( - url: "http://new.example.com/pic".toLibSession(), - key: "qwert\0yuio1234567890123456789012".data(using: .utf8)!.toLibSession() - ) - user_profile_set_pic(conf2, p2) - - user_profile_set_nts_expiry(conf2, 86400) - expect(user_profile_get_nts_expiry(conf2)).to(equal(86400)) - - expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1)) - user_profile_set_blinded_msgreqs(conf2, 0) - expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(0)) - user_profile_set_blinded_msgreqs(conf2, -1) - expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1)) - user_profile_set_blinded_msgreqs(conf2, 1) - expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1)) - - // Both have changes, so push need a push - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_push(conf2)).to(beTrue()) - - let fakeHash2: String = "fakehash2" - var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() - let pushData3: UnsafeMutablePointer = config_push(conf) - expect(pushData3.pointee.seqno).to(equal(2)) // incremented, since we made a field change - config_confirm_pushed(conf, pushData3.pointee.seqno, &cFakeHash2) - - let fakeHash3: String = "fakehash3" - var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated() - let pushData4: UnsafeMutablePointer = config_push(conf2) - expect(pushData4.pointee.seqno).to(equal(2)) // incremented, since we made a field change - config_confirm_pushed(conf, pushData4.pointee.seqno, &cFakeHash3) - - var dump4: UnsafeMutablePointer? = nil - var dump4Len: Int = 0 - config_dump(conf, &dump4, &dump4Len); - var dump5: UnsafeMutablePointer? = nil - var dump5Len: Int = 0 - config_dump(conf2, &dump5, &dump5Len); - // (store in db) - dump4?.deallocate() - dump5?.deallocate() - - // Since we set different things, we're going to get back different serialized data to be - // pushed: - let pushData3Str: String? = String(pointer: pushData3.pointee.config, length: pushData3.pointee.config_len, encoding: .ascii) - let pushData4Str: String? = String(pointer: pushData4.pointee.config, length: pushData4.pointee.config_len, encoding: .ascii) - expect(pushData3Str).toNot(equal(pushData4Str)) - - // Now imagine that each client pushed its `seqno=2` config to the swarm, but then each client - // also fetches new messages and pulls down the other client's `seqno=2` value. - - // Feed the new config into each other. (This array could hold multiple configs if we pulled - // down more than one). - var mergeHashes2: [UnsafePointer?] = [cFakeHash2].unsafeCopy() - var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData3.pointee.config)] - var mergeSize2: [Int] = [pushData3.pointee.config_len] - expect(config_merge(conf2, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1)) - pushData3.deallocate() - var mergeHashes3: [UnsafePointer?] = [cFakeHash3].unsafeCopy() - var mergeData3: [UnsafePointer?] = [UnsafePointer(pushData4.pointee.config)] - var mergeSize3: [Int] = [pushData4.pointee.config_len] - expect(config_merge(conf, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1)) - pushData4.deallocate() - - // Now after the merge we *will* want to push from both client, since both will have generated a - // merge conflict update (with seqno = 3). - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_push(conf2)).to(beTrue()) - let pushData5: UnsafeMutablePointer = config_push(conf) - let pushData6: UnsafeMutablePointer = config_push(conf2) - expect(pushData5.pointee.seqno).to(equal(3)) - expect(pushData6.pointee.seqno).to(equal(3)) - - // They should have resolved the conflict to the same thing: - expect(String(cString: user_profile_get_name(conf)!)).to(equal("Nibbler")) - expect(String(cString: user_profile_get_name(conf2)!)).to(equal("Nibbler")) - // (Note that they could have also both resolved to "Raz" here, but the hash of the serialized - // message just happens to have a higher hash -- and thus gets priority -- for this particular - // test). - - // Since only one of them set a profile pic there should be no conflict there: - let pic3: user_profile_pic = user_profile_get_pic(conf) - expect(pic3.url).toNot(beNil()) - expect(String(libSessionVal: pic3.url)).to(equal("http://new.example.com/pic")) - expect(pic3.key).toNot(beNil()) - expect(Data(libSessionVal: pic3.key, count: 32).toHexString()) - .to(equal("7177657274007975696f31323334353637383930313233343536373839303132")) - let pic4: user_profile_pic = user_profile_get_pic(conf2) - expect(pic4.url).toNot(beNil()) - expect(String(libSessionVal: pic4.url)).to(equal("http://new.example.com/pic")) - expect(pic4.key).toNot(beNil()) - expect(Data(libSessionVal: pic4.key, count: 32).toHexString()) - .to(equal("7177657274007975696f31323334353637383930313233343536373839303132")) - expect(user_profile_get_nts_priority(conf)).to(equal(9)) - expect(user_profile_get_nts_priority(conf2)).to(equal(9)) - expect(user_profile_get_nts_expiry(conf)).to(equal(86400)) - expect(user_profile_get_nts_expiry(conf2)).to(equal(86400)) - expect(user_profile_get_blinded_msgreqs(conf)).to(equal(1)) - expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1)) - - let fakeHash4: String = "fakehash4" - var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() - let fakeHash5: String = "fakehash5" - var cFakeHash5: [CChar] = fakeHash5.cArray.nullTerminated() - config_confirm_pushed(conf, pushData5.pointee.seqno, &cFakeHash4) - config_confirm_pushed(conf2, pushData6.pointee.seqno, &cFakeHash5) - pushData5.deallocate() - pushData6.deallocate() - - var dump6: UnsafeMutablePointer? = nil - var dump6Len: Int = 0 - config_dump(conf, &dump6, &dump6Len); - var dump7: UnsafeMutablePointer? = nil - var dump7Len: Int = 0 - config_dump(conf2, &dump7, &dump7Len); - // (store in db) - dump6?.deallocate() - dump7?.deallocate() - - expect(config_needs_dump(conf)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_push(conf2)).to(beFalse()) - - // Wouldn't do this in a normal session but doing it here to properly clean up - // after the test - conf?.deallocate() - conf2?.deallocate() - } - } - } -} diff --git a/SessionMessagingKitTests/LibSessionUtil/LibSessionSpec.swift b/SessionMessagingKitTests/LibSessionUtil/LibSessionSpec.swift index 42d0746d7..af6726f5d 100644 --- a/SessionMessagingKitTests/LibSessionUtil/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/LibSessionSpec.swift @@ -1,6 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation +import GRDB import Sodium import SessionUtil import SessionUtilitiesKit @@ -8,15 +9,1792 @@ import SessionUtilitiesKit import Quick import Nimble -class LibSessionSpec: QuickSpec { - // MARK: - Spec +@testable import SessionMessagingKit - override func spec() { +class LibSessionSpec: QuickSpec { + override class func spec() { + // MARK: - libSession describe("libSession") { - ConfigContactsSpec.spec() - ConfigUserProfileSpec.spec() - ConfigConvoInfoVolatileSpec.spec() - ConfigUserGroupsSpec.spec() + contactsSpec() + userProfileSpec() + convoInfoVolatileSpec() + userGroupsSpec() + + // MARK: -- parses community URLs correctly + it("parses community URLs correctly") { + let result1 = SessionUtil.parseCommunity(url: [ + "https://example.com/", + "SomeRoom?public_key=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + ].joined()) + let result2 = SessionUtil.parseCommunity(url: [ + "HTTPS://EXAMPLE.COM/", + "sOMErOOM?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + ].joined()) + let result3 = SessionUtil.parseCommunity(url: [ + "HTTPS://EXAMPLE.COM/r/", + "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + ].joined()) + let result4 = SessionUtil.parseCommunity(url: [ + "http://example.com/r/", + "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + ].joined()) + let result5 = SessionUtil.parseCommunity(url: [ + "HTTPS://EXAMPLE.com:443/r/", + "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + ].joined()) + let result6 = SessionUtil.parseCommunity(url: [ + "HTTP://EXAMPLE.com:80/r/", + "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + ].joined()) + let result7 = SessionUtil.parseCommunity(url: [ + "http://example.com:80/r/", + "someroom?public_key=ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8" + ].joined()) + let result8 = SessionUtil.parseCommunity(url: [ + "http://example.com:80/r/", + "someroom?public_key=yrtwk3hjixg66yjdeiuauk6p7hy1gtm8tgih55abrpnsxnpm3zzo" + ].joined()) + + expect(result1?.server).to(equal("https://example.com")) + expect(result1?.server).to(equal(result2?.server)) + expect(result1?.server).to(equal(result3?.server)) + expect(result1?.server).toNot(equal(result4?.server)) + expect(result4?.server).to(equal("http://example.com")) + expect(result1?.server).to(equal(result5?.server)) + expect(result4?.server).to(equal(result6?.server)) + expect(result4?.server).to(equal(result7?.server)) + expect(result4?.server).to(equal(result8?.server)) + expect(result1?.room).to(equal("SomeRoom")) + expect(result2?.room).to(equal("sOMErOOM")) + expect(result3?.room).to(equal("someroom")) + expect(result4?.room).to(equal("someroom")) + expect(result5?.room).to(equal("someroom")) + expect(result6?.room).to(equal("someroom")) + expect(result7?.room).to(equal("someroom")) + expect(result8?.room).to(equal("someroom")) + expect(result1?.publicKey) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + expect(result2?.publicKey) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + expect(result3?.publicKey) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + expect(result4?.publicKey) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + expect(result5?.publicKey) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + expect(result6?.publicKey) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + expect(result7?.publicKey) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + expect(result8?.publicKey) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + } + } + } +} + +// MARK: - CONTACTS + +fileprivate extension LibSessionSpec { + enum ContactProperty: CaseIterable { + case name + case nickname + case approved + case approved_me + case blocked + case profile_pic + case created + case notifications + case mute_until + } + + class func contactsSpec() { + context("CONTACTS") { + // MARK: -- when checking error catching + context("when checking error catching") { + var seed: Data! + var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)! + var edSK: [UInt8]! + var error: UnsafeMutablePointer? + var conf: UnsafeMutablePointer? + + beforeEach { + seed = Data(hex: "0123456789abcdef0123456789abcdef") + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + identity = try! Identity.generate(from: seed) + edSK = identity.ed25519KeyPair.secretKey + + // Initialize a brand new, empty config because we have no dump data to deal with. + error = nil + conf = nil + _ = contacts_init(&conf, &edSK, nil, 0, error) + error?.deallocate() + } + + // MARK: ---- it can catch size limit errors thrown when pushing + it("can catch size limit errors thrown when pushing") { + var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) + + try (0..<10000).forEach { index in + var contact: contacts_contact = try createContact( + for: index, + in: conf, + rand: &randomGenerator, + maxing: .allProperties + ) + contacts_set(conf, &contact) + } + + expect(contacts_size(conf)).to(equal(10000)) + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_dump(conf)).to(beTrue()) + + expect { + try CExceptionHelper.performSafely { config_push(conf).deallocate() } + } + .to(throwError(NSError(domain: "cpp_exception", code: -2, userInfo: ["NSLocalizedDescription": "Config data is too large"]))) + } + } + + // MARK: -- when checking size limits + context("when checking size limits") { + var numRecords: Int! + var seed: Data! + var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)! + var edSK: [UInt8]! + var error: UnsafeMutablePointer? + var conf: UnsafeMutablePointer? + + beforeEach { + numRecords = 0 + seed = Data(hex: "0123456789abcdef0123456789abcdef") + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + identity = try! Identity.generate(from: seed) + edSK = identity.ed25519KeyPair.secretKey + + // Initialize a brand new, empty config because we have no dump data to deal with. + error = nil + conf = nil + _ = contacts_init(&conf, &edSK, nil, 0, error) + error?.deallocate() + } + + // MARK: ---- has not changed the max empty records + it("has not changed the max empty records") { + var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) + + for index in (0..<100000) { + var contact: contacts_contact = try createContact( + for: index, + in: conf, + rand: &randomGenerator + ) + contacts_set(conf, &contact) + + do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } + catch { break } + + // We successfully inserted a contact and didn't hit the limit so increment the counter + numRecords += 1 + } + + // Check that the record count matches the maximum when we last checked + expect(numRecords).to(equal(2370)) + } + + // MARK: ---- has not changed the max name only records + it("has not changed the max name only records") { + var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) + + for index in (0..<100000) { + var contact: contacts_contact = try createContact( + for: index, + in: conf, + rand: &randomGenerator, + maxing: [.name] + ) + contacts_set(conf, &contact) + + do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } + catch { break } + + // We successfully inserted a contact and didn't hit the limit so increment the counter + numRecords += 1 + } + + // Check that the record count matches the maximum when we last checked + expect(numRecords).to(equal(796)) + } + + // MARK: ---- has not changed the max name and profile pic only records + it("has not changed the max name and profile pic only records") { + var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) + + for index in (0..<100000) { + var contact: contacts_contact = try createContact( + for: index, + in: conf, + rand: &randomGenerator, + maxing: [.name, .profile_pic] + ) + contacts_set(conf, &contact) + + do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } + catch { break } + + // We successfully inserted a contact and didn't hit the limit so increment the counter + numRecords += 1 + } + + // Check that the record count matches the maximum when we last checked + expect(numRecords).to(equal(290)) + } + + // MARK: ---- has not changed the max filled records + it("has not changed the max filled records") { + var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) + + for index in (0..<100000) { + var contact: contacts_contact = try createContact( + for: index, + in: conf, + rand: &randomGenerator, + maxing: .allProperties + ) + contacts_set(conf, &contact) + + do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } + catch { break } + + // We successfully inserted a contact and didn't hit the limit so increment the counter + numRecords += 1 + } + + // Check that the record count matches the maximum when we last checked + expect(numRecords).to(equal(236)) + } + } + + // MARK: -- generates config correctly + + it("generates config correctly") { + let createdTs: Int64 = 1680064059 + let nowTs: Int64 = Int64(Date().timeIntervalSince1970) + let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + let identity = try! Identity.generate(from: seed) + var edSK: [UInt8] = identity.ed25519KeyPair.secretKey + expect(edSK.toHexString().suffix(64)) + .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) + expect(identity.x25519KeyPair.publicKey.toHexString()) + .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) + expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) + + // Initialize a brand new, empty config because we have no dump data to deal with. + let error: UnsafeMutablePointer? = nil + var conf: UnsafeMutablePointer? = nil + expect(contacts_init(&conf, &edSK, nil, 0, error)).to(equal(0)) + error?.deallocate() + + // Empty contacts shouldn't have an existing contact + let definitelyRealId: String = "050000000000000000000000000000000000000000000000000000000000000000" + var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() + let contactPtr: UnsafeMutablePointer? = nil + expect(contacts_get(conf, contactPtr, &cDefinitelyRealId)).to(beFalse()) + + expect(contacts_size(conf)).to(equal(0)) + + var contact2: contacts_contact = contacts_contact() + expect(contacts_get_or_construct(conf, &contact2, &cDefinitelyRealId)).to(beTrue()) + expect(String(libSessionVal: contact2.name)).to(beEmpty()) + expect(String(libSessionVal: contact2.nickname)).to(beEmpty()) + expect(contact2.approved).to(beFalse()) + expect(contact2.approved_me).to(beFalse()) + expect(contact2.blocked).to(beFalse()) + expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently + expect(String(libSessionVal: contact2.profile_pic.url)).to(beEmpty()) + expect(contact2.created).to(equal(0)) + expect(contact2.notifications).to(equal(CONVO_NOTIFY_DEFAULT)) + expect(contact2.mute_until).to(equal(0)) + + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_dump(conf)).to(beFalse()) + + let pushData1: UnsafeMutablePointer = config_push(conf) + expect(pushData1.pointee.seqno).to(equal(0)) + pushData1.deallocate() + + // Update the contact data + contact2.name = "Joe".toLibSession() + contact2.nickname = "Joey".toLibSession() + contact2.approved = true + contact2.approved_me = true + contact2.created = createdTs + contact2.notifications = CONVO_NOTIFY_ALL + contact2.mute_until = nowTs + 1800 + + // Update the contact + contacts_set(conf, &contact2) + + // Ensure the contact details were updated + var contact3: contacts_contact = contacts_contact() + expect(contacts_get(conf, &contact3, &cDefinitelyRealId)).to(beTrue()) + expect(String(libSessionVal: contact3.name)).to(equal("Joe")) + expect(String(libSessionVal: contact3.nickname)).to(equal("Joey")) + expect(contact3.approved).to(beTrue()) + expect(contact3.approved_me).to(beTrue()) + expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently + expect(String(libSessionVal: contact3.profile_pic.url)).to(beEmpty()) + expect(contact3.blocked).to(beFalse()) + expect(String(libSessionVal: contact3.session_id)).to(equal(definitelyRealId)) + expect(contact3.created).to(equal(createdTs)) + expect(contact2.notifications).to(equal(CONVO_NOTIFY_ALL)) + expect(contact2.mute_until).to(equal(nowTs + 1800)) + + + // Since we've made changes, we should need to push new config to the swarm, *and* should need + // to dump the updated state: + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_dump(conf)).to(beTrue()) + + // incremented since we made changes (this only increments once between + // dumps; even though we changed multiple fields here). + let pushData2: UnsafeMutablePointer = config_push(conf) + + // incremented since we made changes (this only increments once between + // dumps; even though we changed multiple fields here). + expect(pushData2.pointee.seqno).to(equal(1)) + + // Pretend we uploaded it + let fakeHash1: String = "fakehash1" + var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() + config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_dump(conf)).to(beTrue()) + pushData2.deallocate() + + // NB: Not going to check encrypted data and decryption here because that's general (not + // specific to contacts) and is covered already in the user profile tests. + var dump1: UnsafeMutablePointer? = nil + var dump1Len: Int = 0 + config_dump(conf, &dump1, &dump1Len) + + let error2: UnsafeMutablePointer? = nil + var conf2: UnsafeMutablePointer? = nil + expect(contacts_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0)) + error2?.deallocate() + dump1?.deallocate() + + expect(config_needs_push(conf2)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + + let pushData3: UnsafeMutablePointer = config_push(conf2) + expect(pushData3.pointee.seqno).to(equal(1)) + pushData3.deallocate() + + // Because we just called dump() above, to load up contacts2 + expect(config_needs_dump(conf)).to(beFalse()) + + // Ensure the contact details were updated + var contact4: contacts_contact = contacts_contact() + expect(contacts_get(conf2, &contact4, &cDefinitelyRealId)).to(beTrue()) + expect(String(libSessionVal: contact4.name)).to(equal("Joe")) + expect(String(libSessionVal: contact4.nickname)).to(equal("Joey")) + expect(contact4.approved).to(beTrue()) + expect(contact4.approved_me).to(beTrue()) + expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently + expect(String(libSessionVal: contact4.profile_pic.url)).to(beEmpty()) + expect(contact4.blocked).to(beFalse()) + expect(contact4.created).to(equal(createdTs)) + + let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111" + var cAnotherId: [CChar] = anotherId.cArray.nullTerminated() + var contact5: contacts_contact = contacts_contact() + expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue()) + expect(String(libSessionVal: contact5.name)).to(beEmpty()) + expect(String(libSessionVal: contact5.nickname)).to(beEmpty()) + expect(contact5.approved).to(beFalse()) + expect(contact5.approved_me).to(beFalse()) + expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently + expect(String(libSessionVal: contact5.profile_pic.url)).to(beEmpty()) + expect(contact5.blocked).to(beFalse()) + + // We're not setting any fields, but we should still keep a record of the session id + contacts_set(conf2, &contact5) + expect(config_needs_push(conf2)).to(beTrue()) + + let pushData4: UnsafeMutablePointer = config_push(conf2) + expect(pushData4.pointee.seqno).to(equal(2)) + + // Check the merging + let fakeHash2: String = "fakehash2" + var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() + var mergeHashes: [UnsafePointer?] = [cFakeHash2].unsafeCopy() + var mergeData: [UnsafePointer?] = [UnsafePointer(pushData4.pointee.config)] + var mergeSize: [Int] = [pushData4.pointee.config_len] + expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1)) + config_confirm_pushed(conf2, pushData4.pointee.seqno, &cFakeHash2) + mergeHashes.forEach { $0?.deallocate() } + pushData4.deallocate() + + expect(config_needs_push(conf)).to(beFalse()) + + let pushData5: UnsafeMutablePointer = config_push(conf) + expect(pushData5.pointee.seqno).to(equal(2)) + pushData5.deallocate() + + // Iterate through and make sure we got everything we expected + var sessionIds: [String] = [] + var nicknames: [String] = [] + expect(contacts_size(conf)).to(equal(2)) + + var contact6: contacts_contact = contacts_contact() + let contactIterator: UnsafeMutablePointer = contacts_iterator_new(conf) + while !contacts_iterator_done(contactIterator, &contact6) { + sessionIds.append(String(libSessionVal: contact6.session_id)) + nicknames.append(String(libSessionVal: contact6.nickname, nullIfEmpty: true) ?? "(N/A)") + contacts_iterator_advance(contactIterator) + } + contacts_iterator_free(contactIterator) // Need to free the iterator + + expect(sessionIds.count).to(equal(2)) + expect(sessionIds.count).to(equal(contacts_size(conf))) + expect(sessionIds.first).to(equal(definitelyRealId)) + expect(sessionIds.last).to(equal(anotherId)) + expect(nicknames.first).to(equal("Joey")) + expect(nicknames.last).to(equal("(N/A)")) + + // Conflict! Oh no! + + // On client 1 delete a contact: + contacts_erase(conf, definitelyRealId) + + // Client 2 adds a new friend: + let thirdId: String = "052222222222222222222222222222222222222222222222222222222222222222" + var cThirdId: [CChar] = thirdId.cArray.nullTerminated() + var contact7: contacts_contact = contacts_contact() + expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue()) + contact7.nickname = "Nickname 3".toLibSession() + contact7.approved = true + contact7.approved_me = true + contact7.profile_pic.url = "http://example.com/huge.bmp".toLibSession() + contact7.profile_pic.key = "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession() + contacts_set(conf2, &contact7) + + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_push(conf2)).to(beTrue()) + + let pushData6: UnsafeMutablePointer = config_push(conf) + expect(pushData6.pointee.seqno).to(equal(3)) + + let pushData7: UnsafeMutablePointer = config_push(conf2) + expect(pushData7.pointee.seqno).to(equal(3)) + + let pushData6Str: String = String(pointer: pushData6.pointee.config, length: pushData6.pointee.config_len, encoding: .ascii)! + let pushData7Str: String = String(pointer: pushData7.pointee.config, length: pushData7.pointee.config_len, encoding: .ascii)! + expect(pushData6Str).toNot(equal(pushData7Str)) + expect([String](pointer: pushData6.pointee.obsolete, count: pushData6.pointee.obsolete_len)) + .to(equal([fakeHash2])) + expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len)) + .to(equal([fakeHash2])) + + let fakeHash3a: String = "fakehash3a" + var cFakeHash3a: [CChar] = fakeHash3a.cArray.nullTerminated() + let fakeHash3b: String = "fakehash3b" + var cFakeHash3b: [CChar] = fakeHash3b.cArray.nullTerminated() + config_confirm_pushed(conf, pushData6.pointee.seqno, &cFakeHash3a) + config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash3b) + + var mergeHashes2: [UnsafePointer?] = [cFakeHash3b].unsafeCopy() + var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData7.pointee.config)] + var mergeSize2: [Int] = [pushData7.pointee.config_len] + expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1)) + expect(config_needs_push(conf)).to(beTrue()) + + var mergeHashes3: [UnsafePointer?] = [cFakeHash3a].unsafeCopy() + var mergeData3: [UnsafePointer?] = [UnsafePointer(pushData6.pointee.config)] + var mergeSize3: [Int] = [pushData6.pointee.config_len] + expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1)) + expect(config_needs_push(conf2)).to(beTrue()) + mergeHashes2.forEach { $0?.deallocate() } + mergeHashes3.forEach { $0?.deallocate() } + pushData6.deallocate() + pushData7.deallocate() + + let pushData8: UnsafeMutablePointer = config_push(conf) + expect(pushData8.pointee.seqno).to(equal(4)) + + let pushData9: UnsafeMutablePointer = config_push(conf2) + expect(pushData9.pointee.seqno).to(equal(pushData8.pointee.seqno)) + + let pushData8Str: String = String(pointer: pushData8.pointee.config, length: pushData8.pointee.config_len, encoding: .ascii)! + let pushData9Str: String = String(pointer: pushData9.pointee.config, length: pushData9.pointee.config_len, encoding: .ascii)! + expect(pushData8Str).to(equal(pushData9Str)) + expect([String](pointer: pushData8.pointee.obsolete, count: pushData8.pointee.obsolete_len)) + .to(equal([fakeHash3b, fakeHash3a])) + expect([String](pointer: pushData9.pointee.obsolete, count: pushData9.pointee.obsolete_len)) + .to(equal([fakeHash3a, fakeHash3b])) + + let fakeHash4: String = "fakeHash4" + var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() + config_confirm_pushed(conf, pushData8.pointee.seqno, &cFakeHash4) + config_confirm_pushed(conf2, pushData9.pointee.seqno, &cFakeHash4) + pushData8.deallocate() + pushData9.deallocate() + + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_push(conf2)).to(beFalse()) + + // Validate the changes + var sessionIds2: [String] = [] + var nicknames2: [String] = [] + expect(contacts_size(conf)).to(equal(2)) + + var contact8: contacts_contact = contacts_contact() + let contactIterator2: UnsafeMutablePointer = contacts_iterator_new(conf) + while !contacts_iterator_done(contactIterator2, &contact8) { + sessionIds2.append(String(libSessionVal: contact8.session_id)) + nicknames2.append(String(libSessionVal: contact8.nickname, nullIfEmpty: true) ?? "(N/A)") + contacts_iterator_advance(contactIterator2) + } + contacts_iterator_free(contactIterator2) // Need to free the iterator + + expect(sessionIds2.count).to(equal(2)) + expect(sessionIds2.first).to(equal(anotherId)) + expect(sessionIds2.last).to(equal(thirdId)) + expect(nicknames2.first).to(equal("(N/A)")) + expect(nicknames2.last).to(equal("Nickname 3")) + } + } + } + + // MARK: - Convenience + + private static func createContact( + for index: Int, + in conf: UnsafeMutablePointer?, + rand: inout ARC4RandomNumberGenerator, + maxing properties: [ContactProperty] = [] + ) throws -> contacts_contact { + let postPrefixId: String = "05\(rand.nextBytes(count: 32).toHexString())" + let sessionId: String = ("05\(index)a" + postPrefixId.suffix(postPrefixId.count - "05\(index)a".count)) + var cSessionId: [CChar] = sessionId.cArray.nullTerminated() + var contact: contacts_contact = contacts_contact() + + guard contacts_get_or_construct(conf, &contact, &cSessionId) else { + throw SessionUtilError.getOrConstructFailedUnexpectedly + } + + // Set the values to the maximum data that can fit + properties.forEach { property in + switch property { + case .approved: contact.approved = true + case .approved_me: contact.approved_me = true + case .blocked: contact.blocked = true + case .created: contact.created = Int64.max + case .notifications: contact.notifications = CONVO_NOTIFY_MENTIONS_ONLY + case .mute_until: contact.mute_until = Int64.max + + case .name: + contact.name = rand.nextBytes(count: SessionUtil.libSessionMaxNameByteLength) + .toHexString() + .toLibSession() + + case .nickname: + contact.nickname = rand.nextBytes(count: SessionUtil.libSessionMaxNameByteLength) + .toHexString() + .toLibSession() + + case .profile_pic: + contact.profile_pic = user_profile_pic( + url: rand.nextBytes(count: SessionUtil.libSessionMaxProfileUrlByteLength) + .toHexString() + .toLibSession(), + key: Data(rand.nextBytes(count: 32)) + .toLibSession() + ) + } + } + + return contact + } +} + +fileprivate extension Array where Element == LibSessionSpec.ContactProperty { + static var allProperties: [LibSessionSpec.ContactProperty] = LibSessionSpec.ContactProperty.allCases +} + +// MARK: - USER_PROFILE + +fileprivate extension LibSessionSpec { + class func userProfileSpec() { + context("USER_PROFILE") { + // MARK: -- generates config correctly + it("generates config correctly") { + let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + let identity = try! Identity.generate(from: seed) + var edSK: [UInt8] = identity.ed25519KeyPair.secretKey + expect(edSK.toHexString().suffix(64)) + .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) + expect(identity.x25519KeyPair.publicKey.toHexString()) + .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) + expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) + + // Initialize a brand new, empty config because we have no dump data to deal with. + let error: UnsafeMutablePointer? = nil + var conf: UnsafeMutablePointer? = nil + expect(user_profile_init(&conf, &edSK, nil, 0, error)).to(equal(0)) + error?.deallocate() + + // We don't need to push anything, since this is an empty config + expect(config_needs_push(conf)).to(beFalse()) + // And we haven't changed anything so don't need to dump to db + expect(config_needs_dump(conf)).to(beFalse()) + + // Since it's empty there shouldn't be a name. + let namePtr: UnsafePointer? = user_profile_get_name(conf) + expect(namePtr).to(beNil()) + + // We don't need to push since we haven't changed anything, so this call is mainly just for + // testing: + let pushData1: UnsafeMutablePointer = config_push(conf) + expect(pushData1.pointee).toNot(beNil()) + expect(pushData1.pointee.seqno).to(equal(0)) + expect(pushData1.pointee.config_len).to(equal(256)) + + let encDomain: [CChar] = "UserProfile" + .bytes + .map { CChar(bitPattern: $0) } + expect(String(cString: config_encryption_domain(conf))).to(equal("UserProfile")) + + var toPushDecSize: Int = 0 + let toPushDecrypted: UnsafeMutablePointer? = config_decrypt(pushData1.pointee.config, pushData1.pointee.config_len, edSK, encDomain, &toPushDecSize) + let prefixPadding: String = (0..<193) + .map { _ in "\0" } + .joined() + expect(toPushDecrypted).toNot(beNil()) + expect(toPushDecSize).to(equal(216)) // 256 - 40 overhead + expect(String(pointer: toPushDecrypted, length: toPushDecSize)) + .to(equal("\(prefixPadding)d1:#i0e1:&de1:? = user_profile_get_name(conf) + expect(namePtr2).toNot(beNil()) + expect(String(cString: namePtr2!)).to(equal("Kallie")) + + let pic2: user_profile_pic = user_profile_get_pic(conf); + expect(String(libSessionVal: pic2.url)).to(equal("http://example.org/omg-pic-123.bmp")) + expect(Data(libSessionVal: pic2.key, count: ProfileManager.avatarAES256KeyByteLength)) + .to(equal("secret78901234567890123456789012".data(using: .utf8))) + expect(user_profile_get_nts_priority(conf)).to(equal(9)) + + // Since we've made changes, we should need to push new config to the swarm, *and* should need + // to dump the updated state: + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_dump(conf)).to(beTrue()) + + // incremented since we made changes (this only increments once between + // dumps; even though we changed two fields here). + let pushData2: UnsafeMutablePointer = config_push(conf) + expect(pushData2.pointee.seqno).to(equal(1)) + + // Note: This hex value differs from the value in the library tests because + // it looks like the library has an "end of cell mark" character added at the + // end (0x07 or '0007') so we need to manually add it to work + let expHash0: [UInt8] = Data(hex: "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965") + .bytes + // The data to be actually pushed, expanded like this to make it somewhat human-readable: + let expPush1Decrypted: [UInt8] = [""" + d + 1:#i1e + 1:& d + 1:+ i9e + 1:n 6:Kallie + 1:p 34:http://example.org/omg-pic-123.bmp + 1:q 32:secret78901234567890123456789012 + e + 1:< l + l i0e 32: + """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability + .bytes, + expHash0, + """ + de e + e + 1:= d + 1:+ 0: + 1:n 0: + 1:p 0: + 1:q 0: + e + e + """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability + .bytes + ].flatMap { $0 } + let expPush1Encrypted: [UInt8] = Data(hex: [ + "9693a69686da3055f1ecdfb239c3bf8e746951a36d888c2fb7c02e856a5c2091b24e39a7e1af828f", + "1fa09fe8bf7d274afde0a0847ba143c43ffb8722301b5ae32e2f078b9a5e19097403336e50b18c84", + "aade446cd2823b011f97d6ad2116a53feb814efecc086bc172d31f4214b4d7c630b63bbe575b0868", + "2d146da44915063a07a78556ab5eff4f67f6aa26211e8d330b53d28567a931028c393709a325425d", + "e7486ccde24416a7fd4a8ba5fa73899c65f4276dfaddd5b2100adcf0f793104fb235b31ce32ec656", + "056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea", + "49bf122762d7bc1d6d9c02f6d54f8384" + ].joined()).bytes + + let pushData2Str: String = String(pointer: pushData2.pointee.config, length: pushData2.pointee.config_len, encoding: .ascii)! + let expPush1EncryptedStr: String = String(pointer: expPush1Encrypted, length: expPush1Encrypted.count, encoding: .ascii)! + expect(pushData2Str).to(equal(expPush1EncryptedStr)) + + // Raw decryption doesn't unpad (i.e. the padding is part of the encrypted data) + var pushData2DecSize: Int = 0 + let pushData2Decrypted: UnsafeMutablePointer? = config_decrypt( + pushData2.pointee.config, + pushData2.pointee.config_len, + edSK, + encDomain, + &pushData2DecSize + ) + let prefixPadding2: String = (0..<(256 - 40 - expPush1Decrypted.count)) + .map { _ in "\0" } + .joined() + expect(pushData2DecSize).to(equal(216)) // 256 - 40 overhead + + let pushData2DecryptedStr: String = String(pointer: pushData2Decrypted, length: pushData2DecSize, encoding: .ascii)! + let expPush1DecryptedStr: String = String(pointer: expPush1Decrypted, length: expPush1Decrypted.count, encoding: .ascii) + .map { "\(prefixPadding2)\($0)" }! + expect(pushData2DecryptedStr).to(equal(expPush1DecryptedStr)) + pushData2Decrypted?.deallocate() + + // We haven't dumped, so still need to dump: + expect(config_needs_dump(conf)).to(beTrue()) + // We did call push, but we haven't confirmed it as stored yet, so this will still return true: + expect(config_needs_push(conf)).to(beTrue()) + + var dump1: UnsafeMutablePointer? = nil + var dump1Len: Int = 0 + + config_dump(conf, &dump1, &dump1Len) + // (in a real client we'd now store this to disk) + + expect(config_needs_dump(conf)).to(beFalse()) + + let expDump1: [CChar] = [ + """ + d + 1:! i2e + 1:$ \(expPush1Decrypted.count): + """ + .removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) + .bytes + .map { CChar(bitPattern: $0) }, + expPush1Decrypted + .map { CChar(bitPattern: $0) }, + """ + 1:(0: + 1:)le + e + """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) + .bytes + .map { CChar(bitPattern: $0) } + ].flatMap { $0 } + expect(String(pointer: dump1, length: dump1Len, encoding: .ascii)) + .to(equal(String(pointer: expDump1, length: expDump1.count, encoding: .ascii))) + dump1?.deallocate() + + // So now imagine we got back confirmation from the swarm that the push has been stored: + let fakeHash1: String = "fakehash1" + var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() + config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) + pushData2.deallocate() + + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_dump(conf)).to(beTrue()) // The confirmation changes state, so this makes us need a dump + + var dump2: UnsafeMutablePointer? = nil + var dump2Len: Int = 0 + config_dump(conf, &dump2, &dump2Len) + + let expDump2: [CChar] = [ + """ + d + 1:! i0e + 1:$ \(expPush1Decrypted.count): + """ + .removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) + .bytes + .map { CChar(bitPattern: $0) }, + expPush1Decrypted + .map { CChar(bitPattern: $0) }, + """ + 1:(9:fakehash1 + 1:)le + e + """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) + .bytes + .map { CChar(bitPattern: $0) } + ].flatMap { $0 } + expect(String(pointer: dump2, length: dump2Len, encoding: .ascii)) + .to(equal(String(pointer: expDump2, length: expDump2.count, encoding: .ascii))) + dump2?.deallocate() + expect(config_needs_dump(conf)).to(beFalse()) + + // Now we're going to set up a second, competing config object (in the real world this would be + // another Session client somewhere). + + // Start with an empty config, as above: + let error2: UnsafeMutablePointer? = nil + var conf2: UnsafeMutablePointer? = nil + expect(user_profile_init(&conf2, &edSK, nil, 0, error2)).to(equal(0)) + expect(config_needs_dump(conf2)).to(beFalse()) + error2?.deallocate() + + // Now imagine we just pulled down the `exp_push1` string from the swarm; we merge it into + // conf2: + var mergeHashes: [UnsafePointer?] = [cFakeHash1].unsafeCopy() + var mergeData: [UnsafePointer?] = [expPush1Encrypted].unsafeCopy() + var mergeSize: [Int] = [expPush1Encrypted.count] + expect(config_merge(conf2, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1)) + mergeHashes.forEach { $0?.deallocate() } + mergeData.forEach { $0?.deallocate() } + + // Our state has changed, so we need to dump: + expect(config_needs_dump(conf2)).to(beTrue()) + var dump3: UnsafeMutablePointer? = nil + var dump3Len: Int = 0 + config_dump(conf2, &dump3, &dump3Len) + // (store in db) + dump3?.deallocate() + expect(config_needs_dump(conf2)).to(beFalse()) + + // We *don't* need to push: even though we updated, all we did is update to the merged data (and + // didn't have any sort of merge conflict needed): + expect(config_needs_push(conf2)).to(beFalse()) + + // Now let's create a conflicting update: + + // Change the name on both clients: + user_profile_set_name(conf, "Nibbler") + user_profile_set_name(conf2, "Raz") + + // And, on conf2, we're also going to change the profile pic: + let p2: user_profile_pic = user_profile_pic( + url: "http://new.example.com/pic".toLibSession(), + key: "qwert\0yuio1234567890123456789012".data(using: .utf8)!.toLibSession() + ) + user_profile_set_pic(conf2, p2) + + user_profile_set_nts_expiry(conf2, 86400) + expect(user_profile_get_nts_expiry(conf2)).to(equal(86400)) + + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1)) + user_profile_set_blinded_msgreqs(conf2, 0) + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(0)) + user_profile_set_blinded_msgreqs(conf2, -1) + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1)) + user_profile_set_blinded_msgreqs(conf2, 1) + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1)) + + // Both have changes, so push need a push + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_push(conf2)).to(beTrue()) + + let fakeHash2: String = "fakehash2" + var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() + let pushData3: UnsafeMutablePointer = config_push(conf) + expect(pushData3.pointee.seqno).to(equal(2)) // incremented, since we made a field change + config_confirm_pushed(conf, pushData3.pointee.seqno, &cFakeHash2) + + let fakeHash3: String = "fakehash3" + var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated() + let pushData4: UnsafeMutablePointer = config_push(conf2) + expect(pushData4.pointee.seqno).to(equal(2)) // incremented, since we made a field change + config_confirm_pushed(conf, pushData4.pointee.seqno, &cFakeHash3) + + var dump4: UnsafeMutablePointer? = nil + var dump4Len: Int = 0 + config_dump(conf, &dump4, &dump4Len); + var dump5: UnsafeMutablePointer? = nil + var dump5Len: Int = 0 + config_dump(conf2, &dump5, &dump5Len); + // (store in db) + dump4?.deallocate() + dump5?.deallocate() + + // Since we set different things, we're going to get back different serialized data to be + // pushed: + let pushData3Str: String? = String(pointer: pushData3.pointee.config, length: pushData3.pointee.config_len, encoding: .ascii) + let pushData4Str: String? = String(pointer: pushData4.pointee.config, length: pushData4.pointee.config_len, encoding: .ascii) + expect(pushData3Str).toNot(equal(pushData4Str)) + + // Now imagine that each client pushed its `seqno=2` config to the swarm, but then each client + // also fetches new messages and pulls down the other client's `seqno=2` value. + + // Feed the new config into each other. (This array could hold multiple configs if we pulled + // down more than one). + var mergeHashes2: [UnsafePointer?] = [cFakeHash2].unsafeCopy() + var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData3.pointee.config)] + var mergeSize2: [Int] = [pushData3.pointee.config_len] + expect(config_merge(conf2, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1)) + pushData3.deallocate() + var mergeHashes3: [UnsafePointer?] = [cFakeHash3].unsafeCopy() + var mergeData3: [UnsafePointer?] = [UnsafePointer(pushData4.pointee.config)] + var mergeSize3: [Int] = [pushData4.pointee.config_len] + expect(config_merge(conf, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1)) + pushData4.deallocate() + + // Now after the merge we *will* want to push from both client, since both will have generated a + // merge conflict update (with seqno = 3). + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_push(conf2)).to(beTrue()) + let pushData5: UnsafeMutablePointer = config_push(conf) + let pushData6: UnsafeMutablePointer = config_push(conf2) + expect(pushData5.pointee.seqno).to(equal(3)) + expect(pushData6.pointee.seqno).to(equal(3)) + + // They should have resolved the conflict to the same thing: + expect(String(cString: user_profile_get_name(conf)!)).to(equal("Nibbler")) + expect(String(cString: user_profile_get_name(conf2)!)).to(equal("Nibbler")) + // (Note that they could have also both resolved to "Raz" here, but the hash of the serialized + // message just happens to have a higher hash -- and thus gets priority -- for this particular + // test). + + // Since only one of them set a profile pic there should be no conflict there: + let pic3: user_profile_pic = user_profile_get_pic(conf) + expect(pic3.url).toNot(beNil()) + expect(String(libSessionVal: pic3.url)).to(equal("http://new.example.com/pic")) + expect(pic3.key).toNot(beNil()) + expect(Data(libSessionVal: pic3.key, count: 32).toHexString()) + .to(equal("7177657274007975696f31323334353637383930313233343536373839303132")) + let pic4: user_profile_pic = user_profile_get_pic(conf2) + expect(pic4.url).toNot(beNil()) + expect(String(libSessionVal: pic4.url)).to(equal("http://new.example.com/pic")) + expect(pic4.key).toNot(beNil()) + expect(Data(libSessionVal: pic4.key, count: 32).toHexString()) + .to(equal("7177657274007975696f31323334353637383930313233343536373839303132")) + expect(user_profile_get_nts_priority(conf)).to(equal(9)) + expect(user_profile_get_nts_priority(conf2)).to(equal(9)) + expect(user_profile_get_nts_expiry(conf)).to(equal(86400)) + expect(user_profile_get_nts_expiry(conf2)).to(equal(86400)) + expect(user_profile_get_blinded_msgreqs(conf)).to(equal(1)) + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1)) + + let fakeHash4: String = "fakehash4" + var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() + let fakeHash5: String = "fakehash5" + var cFakeHash5: [CChar] = fakeHash5.cArray.nullTerminated() + config_confirm_pushed(conf, pushData5.pointee.seqno, &cFakeHash4) + config_confirm_pushed(conf2, pushData6.pointee.seqno, &cFakeHash5) + pushData5.deallocate() + pushData6.deallocate() + + var dump6: UnsafeMutablePointer? = nil + var dump6Len: Int = 0 + config_dump(conf, &dump6, &dump6Len); + var dump7: UnsafeMutablePointer? = nil + var dump7Len: Int = 0 + config_dump(conf2, &dump7, &dump7Len); + // (store in db) + dump6?.deallocate() + dump7?.deallocate() + + expect(config_needs_dump(conf)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_push(conf2)).to(beFalse()) + + // Wouldn't do this in a normal session but doing it here to properly clean up + // after the test + conf?.deallocate() + conf2?.deallocate() + } + } + } +} + +// MARK: - CONVO_INFO_VOLATILE + +fileprivate extension LibSessionSpec { + class func convoInfoVolatileSpec() { + context("CONVO_INFO_VOLATILE") { + // MARK: -- generates config correctly + it("generates config correctly") { + let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + let identity = try! Identity.generate(from: seed) + var edSK: [UInt8] = identity.ed25519KeyPair.secretKey + expect(edSK.toHexString().suffix(64)) + .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) + expect(identity.x25519KeyPair.publicKey.toHexString()) + .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) + expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) + + // Initialize a brand new, empty config because we have no dump data to deal with. + let error: UnsafeMutablePointer? = nil + var conf: UnsafeMutablePointer? = nil + expect(convo_info_volatile_init(&conf, &edSK, nil, 0, error)).to(equal(0)) + error?.deallocate() + + // Empty contacts shouldn't have an existing contact + let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000" + var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() + var oneToOne1: convo_info_volatile_1to1 = convo_info_volatile_1to1() + expect(convo_info_volatile_get_1to1(conf, &oneToOne1, &cDefinitelyRealId)).to(beFalse()) + expect(convo_info_volatile_size(conf)).to(equal(0)) + + var oneToOne2: convo_info_volatile_1to1 = convo_info_volatile_1to1() + expect(convo_info_volatile_get_or_construct_1to1(conf, &oneToOne2, &cDefinitelyRealId)) + .to(beTrue()) + expect(String(libSessionVal: oneToOne2.session_id)).to(equal(definitelyRealId)) + expect(oneToOne2.last_read).to(equal(0)) + expect(oneToOne2.unread).to(beFalse()) + + // No need to sync a conversation with a default state + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_dump(conf)).to(beFalse()) + + // Update the last read + let nowTimestampMs: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000)) + oneToOne2.last_read = nowTimestampMs + + // The new data doesn't get stored until we call this: + convo_info_volatile_set_1to1(conf, &oneToOne2) + + var legacyGroup1: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() + var oneToOne3: convo_info_volatile_1to1 = convo_info_volatile_1to1() + expect(convo_info_volatile_get_legacy_group(conf, &legacyGroup1, &cDefinitelyRealId)) + .to(beFalse()) + expect(convo_info_volatile_get_1to1(conf, &oneToOne3, &cDefinitelyRealId)).to(beTrue()) + expect(oneToOne3.last_read).to(equal(nowTimestampMs)) + + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_dump(conf)).to(beTrue()) + + let openGroupBaseUrl: String = "http://Example.ORG:5678" + var cOpenGroupBaseUrl: [CChar] = openGroupBaseUrl.cArray.nullTerminated() + let openGroupBaseUrlResult: String = openGroupBaseUrl.lowercased() + // ("http://Example.ORG:5678" + // .lowercased() + // .cArray + + // [CChar](repeating: 0, count: (268 - openGroupBaseUrl.count)) + // ) + let openGroupRoom: String = "SudokuRoom" + var cOpenGroupRoom: [CChar] = openGroupRoom.cArray.nullTerminated() + let openGroupRoomResult: String = openGroupRoom.lowercased() + // ("SudokuRoom" + // .lowercased() + // .cArray + + // [CChar](repeating: 0, count: (65 - openGroupRoom.count)) + // ) + var cOpenGroupPubkey: [UInt8] = Data(hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + .bytes + var community1: convo_info_volatile_community = convo_info_volatile_community() + expect(convo_info_volatile_get_or_construct_community(conf, &community1, &cOpenGroupBaseUrl, &cOpenGroupRoom, &cOpenGroupPubkey)).to(beTrue()) + expect(String(libSessionVal: community1.base_url)).to(equal(openGroupBaseUrlResult)) + expect(String(libSessionVal: community1.room)).to(equal(openGroupRoomResult)) + expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString()) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + community1.unread = true + + // The new data doesn't get stored until we call this: + convo_info_volatile_set_community(conf, &community1); + + // We don't need to push since we haven't changed anything, so this call is mainly just for + // testing: + let pushData1: UnsafeMutablePointer = config_push(conf) + expect(pushData1.pointee.seqno).to(equal(1)) + + // Pretend we uploaded it + let fakeHash1: String = "fakehash1" + var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() + config_confirm_pushed(conf, pushData1.pointee.seqno, &cFakeHash1) + expect(config_needs_dump(conf)).to(beTrue()) + expect(config_needs_push(conf)).to(beFalse()) + pushData1.deallocate() + + var dump1: UnsafeMutablePointer? = nil + var dump1Len: Int = 0 + config_dump(conf, &dump1, &dump1Len) + + let error2: UnsafeMutablePointer? = nil + var conf2: UnsafeMutablePointer? = nil + expect(convo_info_volatile_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0)) + error2?.deallocate() + dump1?.deallocate() + + expect(config_needs_dump(conf2)).to(beFalse()) + expect(config_needs_push(conf2)).to(beFalse()) + + var oneToOne4: convo_info_volatile_1to1 = convo_info_volatile_1to1() + expect(convo_info_volatile_get_1to1(conf2, &oneToOne4, &cDefinitelyRealId)).to(equal(true)) + expect(oneToOne4.last_read).to(equal(nowTimestampMs)) + expect(String(libSessionVal: oneToOne4.session_id)).to(equal(definitelyRealId)) + expect(oneToOne4.unread).to(beFalse()) + + var community2: convo_info_volatile_community = convo_info_volatile_community() + expect(convo_info_volatile_get_community(conf2, &community2, &cOpenGroupBaseUrl, &cOpenGroupRoom)).to(beTrue()) + expect(String(libSessionVal: community2.base_url)).to(equal(openGroupBaseUrlResult)) + expect(String(libSessionVal: community2.room)).to(equal(openGroupRoomResult)) + expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString()) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + community2.unread = true + + let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111" + var cAnotherId: [CChar] = anotherId.cArray.nullTerminated() + var oneToOne5: convo_info_volatile_1to1 = convo_info_volatile_1to1() + expect(convo_info_volatile_get_or_construct_1to1(conf2, &oneToOne5, &cAnotherId)).to(beTrue()) + oneToOne5.unread = true + convo_info_volatile_set_1to1(conf2, &oneToOne5) + + let thirdId: String = "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + var cThirdId: [CChar] = thirdId.cArray.nullTerminated() + var legacyGroup2: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() + expect(convo_info_volatile_get_or_construct_legacy_group(conf2, &legacyGroup2, &cThirdId)).to(beTrue()) + legacyGroup2.last_read = (nowTimestampMs - 50) + convo_info_volatile_set_legacy_group(conf2, &legacyGroup2) + expect(config_needs_push(conf2)).to(beTrue()) + + let pushData2: UnsafeMutablePointer = config_push(conf2) + expect(pushData2.pointee.seqno).to(equal(2)) + + // Check the merging + let fakeHash2: String = "fakehash2" + var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() + var mergeHashes: [UnsafePointer?] = [cFakeHash2].unsafeCopy() + var mergeData: [UnsafePointer?] = [UnsafePointer(pushData2.pointee.config)] + var mergeSize: [Int] = [pushData2.pointee.config_len] + expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1)) + config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash2) + pushData2.deallocate() + + expect(config_needs_push(conf)).to(beFalse()) + + for targetConf in [conf, conf2] { + // Iterate through and make sure we got everything we expected + var seen: [String] = [] + expect(convo_info_volatile_size(conf)).to(equal(4)) + expect(convo_info_volatile_size_1to1(conf)).to(equal(2)) + expect(convo_info_volatile_size_communities(conf)).to(equal(1)) + expect(convo_info_volatile_size_legacy_groups(conf)).to(equal(1)) + + var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1() + var c2: convo_info_volatile_community = convo_info_volatile_community() + var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() + let it: OpaquePointer = convo_info_volatile_iterator_new(targetConf) + + while !convo_info_volatile_iterator_done(it) { + if convo_info_volatile_it_is_1to1(it, &c1) { + seen.append("1-to-1: \(String(libSessionVal: c1.session_id))") + } + else if convo_info_volatile_it_is_community(it, &c2) { + seen.append("og: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") + } + else if convo_info_volatile_it_is_legacy_group(it, &c3) { + seen.append("cl: \(String(libSessionVal: c3.group_id))") + } + + convo_info_volatile_iterator_advance(it) + } + + convo_info_volatile_iterator_free(it) + + expect(seen).to(equal([ + "1-to-1: 051111111111111111111111111111111111111111111111111111111111111111", + "1-to-1: 055000000000000000000000000000000000000000000000000000000000000000", + "og: http://example.org:5678/r/sudokuroom", + "cl: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ])) + } + + let fourthId: String = "052000000000000000000000000000000000000000000000000000000000000000" + var cFourthId: [CChar] = fourthId.cArray.nullTerminated() + expect(config_needs_push(conf)).to(beFalse()) + convo_info_volatile_erase_1to1(conf, &cFourthId) + expect(config_needs_push(conf)).to(beFalse()) + convo_info_volatile_erase_1to1(conf, &cDefinitelyRealId) + expect(config_needs_push(conf)).to(beTrue()) + expect(convo_info_volatile_size(conf)).to(equal(3)) + expect(convo_info_volatile_size_1to1(conf)).to(equal(1)) + + // Check the single-type iterators: + var seen1: [String?] = [] + var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1() + let it1: OpaquePointer = convo_info_volatile_iterator_new_1to1(conf) + + while !convo_info_volatile_iterator_done(it1) { + expect(convo_info_volatile_it_is_1to1(it1, &c1)).to(beTrue()) + + seen1.append(String(libSessionVal: c1.session_id)) + convo_info_volatile_iterator_advance(it1) + } + + convo_info_volatile_iterator_free(it1) + expect(seen1).to(equal([ + "051111111111111111111111111111111111111111111111111111111111111111" + ])) + + var seen2: [String?] = [] + var c2: convo_info_volatile_community = convo_info_volatile_community() + let it2: OpaquePointer = convo_info_volatile_iterator_new_communities(conf) + + while !convo_info_volatile_iterator_done(it2) { + expect(convo_info_volatile_it_is_community(it2, &c2)).to(beTrue()) + + seen2.append(String(libSessionVal: c2.base_url)) + convo_info_volatile_iterator_advance(it2) + } + + convo_info_volatile_iterator_free(it2) + expect(seen2).to(equal([ + "http://example.org:5678" + ])) + + var seen3: [String?] = [] + var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() + let it3: OpaquePointer = convo_info_volatile_iterator_new_legacy_groups(conf) + + while !convo_info_volatile_iterator_done(it3) { + expect(convo_info_volatile_it_is_legacy_group(it3, &c3)).to(beTrue()) + + seen3.append(String(libSessionVal: c3.group_id)) + convo_info_volatile_iterator_advance(it3) + } + + convo_info_volatile_iterator_free(it3) + expect(seen3).to(equal([ + "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ])) + } + } + } +} + +// MARK: - USER_GROUPS + +fileprivate extension LibSessionSpec { + class func userGroupsSpec() { + context("USER_GROUPS") { + // MARK: -- generates config correctly + it("generates config correctly") { + let createdTs: Int64 = 1680064059 + let nowTs: Int64 = Int64(Date().timeIntervalSince1970) + let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + let identity = try! Identity.generate(from: seed) + var edSK: [UInt8] = identity.ed25519KeyPair.secretKey + expect(edSK.toHexString().suffix(64)) + .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) + expect(identity.x25519KeyPair.publicKey.toHexString()) + .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) + expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) + + // Initialize a brand new, empty config because we have no dump data to deal with. + let error: UnsafeMutablePointer? = nil + var conf: UnsafeMutablePointer? = nil + expect(user_groups_init(&conf, &edSK, nil, 0, error)).to(equal(0)) + error?.deallocate() + + // Empty contacts shouldn't have an existing contact + let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000" + var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() + let legacyGroup1: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cDefinitelyRealId) + expect(legacyGroup1?.pointee).to(beNil()) + expect(user_groups_size(conf)).to(equal(0)) + + let legacyGroup2: UnsafeMutablePointer = user_groups_get_or_construct_legacy_group(conf, &cDefinitelyRealId) + expect(legacyGroup2.pointee).toNot(beNil()) + expect(String(libSessionVal: legacyGroup2.pointee.session_id)) + .to(equal(definitelyRealId)) + expect(legacyGroup2.pointee.disappearing_timer).to(equal(0)) + expect(String(libSessionVal: legacyGroup2.pointee.enc_pubkey, fixedLength: 32)).to(equal("")) + expect(String(libSessionVal: legacyGroup2.pointee.enc_seckey, fixedLength: 32)).to(equal("")) + expect(legacyGroup2.pointee.priority).to(equal(0)) + expect(String(libSessionVal: legacyGroup2.pointee.name)).to(equal("")) + expect(legacyGroup2.pointee.joined_at).to(equal(0)) + expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_DEFAULT)) + expect(legacyGroup2.pointee.mute_until).to(equal(0)) + + // Iterate through and make sure we got everything we expected + var membersSeen1: [String: Bool] = [:] + var memberSessionId1: UnsafePointer? = nil + var memberAdmin1: Bool = false + let membersIt1: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2) + + while ugroups_legacy_members_next(membersIt1, &memberSessionId1, &memberAdmin1) { + membersSeen1[String(cString: memberSessionId1!)] = memberAdmin1 + } + + ugroups_legacy_members_free(membersIt1) + + expect(membersSeen1).to(beEmpty()) + + // No need to sync a conversation with a default state + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_dump(conf)).to(beFalse()) + + // We don't need to push since we haven't changed anything, so this call is mainly just for + // testing: + let pushData1: UnsafeMutablePointer = config_push(conf) + expect(pushData1.pointee.seqno).to(equal(0)) + expect([String](pointer: pushData1.pointee.obsolete, count: pushData1.pointee.obsolete_len)) + .to(beEmpty()) + expect(pushData1.pointee.config_len).to(equal(256)) + pushData1.deallocate() + + let users: [String] = [ + "050000000000000000000000000000000000000000000000000000000000000000", + "051111111111111111111111111111111111111111111111111111111111111111", + "052222222222222222222222222222222222222222222222222222222222222222", + "053333333333333333333333333333333333333333333333333333333333333333", + "054444444444444444444444444444444444444444444444444444444444444444", + "055555555555555555555555555555555555555555555555555555555555555555", + "056666666666666666666666666666666666666666666666666666666666666666" + ] + var cUsers: [[CChar]] = users.map { $0.cArray.nullTerminated() } + legacyGroup2.pointee.name = "Englishmen".toLibSession() + legacyGroup2.pointee.disappearing_timer = 60 + legacyGroup2.pointee.joined_at = createdTs + legacyGroup2.pointee.notifications = CONVO_NOTIFY_ALL + legacyGroup2.pointee.mute_until = (nowTs + 3600) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[0], false)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], true)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[4], true)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[5], false)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beFalse()) + + // Flip to and from admin + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], true)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], false)).to(beTrue()) + + expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[5])).to(beTrue()) + expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[4])).to(beTrue()) + + var membersSeen2: [String: Bool] = [:] + var memberSessionId2: UnsafePointer? = nil + var memberAdmin2: Bool = false + let membersIt2: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2) + + while ugroups_legacy_members_next(membersIt2, &memberSessionId2, &memberAdmin2) { + membersSeen2[String(cString: memberSessionId2!)] = memberAdmin2 + } + + ugroups_legacy_members_free(membersIt2) + + expect(membersSeen2).to(equal([ + "050000000000000000000000000000000000000000000000000000000000000000": false, + "051111111111111111111111111111111111111111111111111111111111111111": false, + "052222222222222222222222222222222222222222222222222222222222222222": true + ])) + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + let groupSeed: Data = Data(hex: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff") + let groupEd25519KeyPair = Sodium().sign.keyPair(seed: groupSeed.bytes)! + let groupX25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: groupEd25519KeyPair.publicKey)! + + // Note: this isn't exactly what Session actually does here for legacy closed + // groups (rather it uses X25519 keys) but for this test the distinction doesn't matter. + legacyGroup2.pointee.enc_pubkey = Data(groupX25519PublicKey).toLibSession() + legacyGroup2.pointee.enc_seckey = Data(groupEd25519KeyPair.secretKey).toLibSession() + legacyGroup2.pointee.priority = 3 + + expect(Data(libSessionVal: legacyGroup2.pointee.enc_pubkey, count: 32).toHexString()) + .to(equal("c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e")) + expect(Data(libSessionVal: legacyGroup2.pointee.enc_seckey, count: 32).toHexString()) + .to(equal("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")) + + // The new data doesn't get stored until we call this: + user_groups_set_free_legacy_group(conf, legacyGroup2) + + let legacyGroup3: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cDefinitelyRealId) + expect(legacyGroup3?.pointee).toNot(beNil()) + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_dump(conf)).to(beTrue()) + ugroups_legacy_group_free(legacyGroup3) + + let communityPubkey: String = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + var cCommunityPubkey: [UInt8] = Data(hex: communityPubkey).cArray + var cCommunityBaseUrl: [CChar] = "http://Example.ORG:5678".cArray.nullTerminated() + var cCommunityRoom: [CChar] = "SudokuRoom".cArray.nullTerminated() + var community1: ugroups_community_info = ugroups_community_info() + expect(user_groups_get_or_construct_community(conf, &community1, &cCommunityBaseUrl, &cCommunityRoom, &cCommunityPubkey)) + .to(beTrue()) + + expect(String(libSessionVal: community1.base_url)).to(equal("http://example.org:5678")) // Note: lower-case + expect(String(libSessionVal: community1.room)).to(equal("SudokuRoom")) // Note: case-preserving + expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString()) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + community1.priority = 14 + + // The new data doesn't get stored until we call this: + user_groups_set_community(conf, &community1) + + // incremented since we made changes (this only increments once between + // dumps; even though we changed two fields here). + let pushData2: UnsafeMutablePointer = config_push(conf) + expect(pushData2.pointee.seqno).to(equal(1)) + expect([String](pointer: pushData2.pointee.obsolete, count: pushData2.pointee.obsolete_len)) + .to(beEmpty()) + + // Pretend we uploaded it + let fakeHash1: String = "fakehash1" + var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() + config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) + expect(config_needs_dump(conf)).to(beTrue()) + expect(config_needs_push(conf)).to(beFalse()) + + var dump1: UnsafeMutablePointer? = nil + var dump1Len: Int = 0 + config_dump(conf, &dump1, &dump1Len) + + let error2: UnsafeMutablePointer? = nil + var conf2: UnsafeMutablePointer? = nil + expect(user_groups_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0)) + error2?.deallocate() + dump1?.deallocate() + + expect(config_needs_dump(conf)).to(beFalse()) // Because we just called dump() above, to load up conf2 + expect(config_needs_push(conf)).to(beFalse()) + + let pushData3: UnsafeMutablePointer = config_push(conf) + expect(pushData3.pointee.seqno).to(equal(1)) + expect([String](pointer: pushData3.pointee.obsolete, count: pushData3.pointee.obsolete_len)) + .to(beEmpty()) + pushData3.deallocate() + + let currentHashes1: UnsafeMutablePointer? = config_current_hashes(conf) + expect([String](pointer: currentHashes1?.pointee.value, count: currentHashes1?.pointee.len)) + .to(equal(["fakehash1"])) + currentHashes1?.deallocate() + + expect(config_needs_push(conf2)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + + let pushData4: UnsafeMutablePointer = config_push(conf2) + expect(pushData4.pointee.seqno).to(equal(1)) + expect(config_needs_dump(conf2)).to(beFalse()) + expect([String](pointer: pushData4.pointee.obsolete, count: pushData4.pointee.obsolete_len)) + .to(beEmpty()) + pushData4.deallocate() + + let currentHashes2: UnsafeMutablePointer? = config_current_hashes(conf2) + expect([String](pointer: currentHashes2?.pointee.value, count: currentHashes2?.pointee.len)) + .to(equal(["fakehash1"])) + currentHashes2?.deallocate() + + expect(user_groups_size(conf2)).to(equal(2)) + expect(user_groups_size_communities(conf2)).to(equal(1)) + expect(user_groups_size_legacy_groups(conf2)).to(equal(1)) + + let legacyGroup4: UnsafeMutablePointer? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId) + expect(legacyGroup4?.pointee).toNot(beNil()) + expect(String(libSessionVal: legacyGroup4?.pointee.enc_pubkey, fixedLength: 32)).to(equal("")) + expect(String(libSessionVal: legacyGroup4?.pointee.enc_seckey, fixedLength: 32)).to(equal("")) + expect(legacyGroup4?.pointee.disappearing_timer).to(equal(60)) + expect(String(libSessionVal: legacyGroup4?.pointee.session_id)).to(equal(definitelyRealId)) + expect(legacyGroup4?.pointee.priority).to(equal(3)) + expect(String(libSessionVal: legacyGroup4?.pointee.name)).to(equal("Englishmen")) + expect(legacyGroup4?.pointee.joined_at).to(equal(createdTs)) + expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_ALL)) + expect(legacyGroup2.pointee.mute_until).to(equal(nowTs + 3600)) + + var membersSeen3: [String: Bool] = [:] + var memberSessionId3: UnsafePointer? = nil + var memberAdmin3: Bool = false + let membersIt3: OpaquePointer = ugroups_legacy_members_begin(legacyGroup4) + + while ugroups_legacy_members_next(membersIt3, &memberSessionId3, &memberAdmin3) { + membersSeen3[String(cString: memberSessionId3!)] = memberAdmin3 + } + + ugroups_legacy_members_free(membersIt3) + ugroups_legacy_group_free(legacyGroup4) + + expect(membersSeen3).to(equal([ + "050000000000000000000000000000000000000000000000000000000000000000": false, + "051111111111111111111111111111111111111111111111111111111111111111": false, + "052222222222222222222222222222222222222222222222222222222222222222": true + ])) + + expect(config_needs_push(conf2)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + + let pushData5: UnsafeMutablePointer = config_push(conf2) + expect(pushData5.pointee.seqno).to(equal(1)) + expect(config_needs_dump(conf2)).to(beFalse()) + pushData5.deallocate() + + for targetConf in [conf, conf2] { + // Iterate through and make sure we got everything we expected + var seen: [String] = [] + + var c1: ugroups_legacy_group_info = ugroups_legacy_group_info() + var c2: ugroups_community_info = ugroups_community_info() + let it: OpaquePointer = user_groups_iterator_new(targetConf) + + while !user_groups_iterator_done(it) { + if user_groups_it_is_legacy_group(it, &c1) { + var memberCount: Int = 0 + var adminCount: Int = 0 + ugroups_legacy_members_count(&c1, &memberCount, &adminCount) + seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members") + } + else if user_groups_it_is_community(it, &c2) { + seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") + } + else { + seen.append("unknown") + } + + user_groups_iterator_advance(it) + } + + user_groups_iterator_free(it) + + expect(seen).to(equal([ + "community: http://example.org:5678/r/SudokuRoom", + "legacy: Englishmen, 1 admins, 2 members" + ])) + } + + var cCommunity2BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated() + var cCommunity2Room: [CChar] = "sudokuRoom".cArray.nullTerminated() + var community2: ugroups_community_info = ugroups_community_info() + expect(user_groups_get_community(conf2, &community2, &cCommunity2BaseUrl, &cCommunity2Room)) + .to(beTrue()) + expect(String(libSessionVal: community2.base_url)).to(equal("http://example.org:5678")) + expect(String(libSessionVal: community2.room)).to(equal("SudokuRoom")) // Case preserved from the stored value, not the input value + expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString()) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + expect(community2.priority).to(equal(14)) + + expect(config_needs_push(conf2)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + + let pushData6: UnsafeMutablePointer = config_push(conf2) + expect(pushData6.pointee.seqno).to(equal(1)) + expect(config_needs_dump(conf2)).to(beFalse()) + pushData6.deallocate() + + community2.room = "sudokuRoom".toLibSession() // Change capitalization + user_groups_set_community(conf2, &community2) + + expect(config_needs_push(conf2)).to(beTrue()) + expect(config_needs_dump(conf2)).to(beTrue()) + + let fakeHash2: String = "fakehash2" + var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() + let pushData7: UnsafeMutablePointer = config_push(conf2) + expect(pushData7.pointee.seqno).to(equal(2)) + config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash2) + expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len)) + .to(equal([fakeHash1])) + + let currentHashes3: UnsafeMutablePointer? = config_current_hashes(conf2) + expect([String](pointer: currentHashes3?.pointee.value, count: currentHashes3?.pointee.len)) + .to(equal([fakeHash2])) + currentHashes3?.deallocate() + + var dump2: UnsafeMutablePointer? = nil + var dump2Len: Int = 0 + config_dump(conf2, &dump2, &dump2Len) + + expect(config_needs_push(conf2)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + + let pushData8: UnsafeMutablePointer = config_push(conf2) + expect(pushData8.pointee.seqno).to(equal(2)) + config_confirm_pushed(conf2, pushData8.pointee.seqno, &cFakeHash2) + expect(config_needs_dump(conf2)).to(beFalse()) + + var mergeHashes1: [UnsafePointer?] = [cFakeHash2].unsafeCopy() + var mergeData1: [UnsafePointer?] = [UnsafePointer(pushData8.pointee.config)] + var mergeSize1: [Int] = [pushData8.pointee.config_len] + expect(config_merge(conf, &mergeHashes1, &mergeData1, &mergeSize1, 1)).to(equal(1)) + pushData8.deallocate() + + var cCommunity3BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated() + var cCommunity3Room: [CChar] = "SudokuRoom".cArray.nullTerminated() + var community3: ugroups_community_info = ugroups_community_info() + expect(user_groups_get_community(conf, &community3, &cCommunity3BaseUrl, &cCommunity3Room)) + .to(beTrue()) + expect(String(libSessionVal: community3.room)).to(equal("sudokuRoom")) // We picked up the capitalization change + + expect(user_groups_size(conf)).to(equal(2)) + expect(user_groups_size_communities(conf)).to(equal(1)) + expect(user_groups_size_legacy_groups(conf)).to(equal(1)) + + let legacyGroup5: UnsafeMutablePointer? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId) + expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[4], false)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[5], true)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[6], true)).to(beTrue()) + expect(ugroups_legacy_member_remove(legacyGroup5, &cUsers[1])).to(beTrue()) + + expect(config_needs_push(conf2)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + + let pushData9: UnsafeMutablePointer = config_push(conf2) + expect(pushData9.pointee.seqno).to(equal(2)) + expect(config_needs_dump(conf2)).to(beFalse()) + pushData9.deallocate() + + user_groups_set_free_legacy_group(conf2, legacyGroup5) + expect(config_needs_push(conf2)).to(beTrue()) + expect(config_needs_dump(conf2)).to(beTrue()) + + var cCommunity4BaseUrl: [CChar] = "http://exAMple.ORG:5678".cArray.nullTerminated() + var cCommunity4Room: [CChar] = "sudokuROOM".cArray.nullTerminated() + user_groups_erase_community(conf2, &cCommunity4BaseUrl, &cCommunity4Room) + + let fakeHash3: String = "fakehash3" + var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated() + let pushData10: UnsafeMutablePointer = config_push(conf2) + config_confirm_pushed(conf2, pushData10.pointee.seqno, &cFakeHash3) + + expect(pushData10.pointee.seqno).to(equal(3)) + expect([String](pointer: pushData10.pointee.obsolete, count: pushData10.pointee.obsolete_len)) + .to(equal([fakeHash2])) + + let currentHashes4: UnsafeMutablePointer? = config_current_hashes(conf2) + expect([String](pointer: currentHashes4?.pointee.value, count: currentHashes4?.pointee.len)) + .to(equal([fakeHash3])) + currentHashes4?.deallocate() + + var mergeHashes2: [UnsafePointer?] = [cFakeHash3].unsafeCopy() + var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData10.pointee.config)] + var mergeSize2: [Int] = [pushData10.pointee.config_len] + expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1)) + + expect(user_groups_size(conf)).to(equal(1)) + expect(user_groups_size_communities(conf)).to(equal(0)) + expect(user_groups_size_legacy_groups(conf)).to(equal(1)) + + var prio: Int32 = 0 + var cBeanstalkBaseUrl: [CChar] = "http://jacksbeanstalk.org".cArray.nullTerminated() + var cBeanstalkPubkey: [UInt8] = Data( + hex: "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" + ).cArray + + ["fee", "fi", "fo", "fum"].forEach { room in + var cRoom: [CChar] = room.cArray.nullTerminated() + prio += 1 + + var community4: ugroups_community_info = ugroups_community_info() + expect(user_groups_get_or_construct_community(conf, &community4, &cBeanstalkBaseUrl, &cRoom, &cBeanstalkPubkey)) + .to(beTrue()) + community4.priority = prio + user_groups_set_community(conf, &community4) + } + + expect(user_groups_size(conf)).to(equal(5)) + expect(user_groups_size_communities(conf)).to(equal(4)) + expect(user_groups_size_legacy_groups(conf)).to(equal(1)) + + let fakeHash4: String = "fakehash4" + var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() + let pushData11: UnsafeMutablePointer = config_push(conf) + config_confirm_pushed(conf, pushData11.pointee.seqno, &cFakeHash4) + expect(pushData11.pointee.seqno).to(equal(4)) + expect([String](pointer: pushData11.pointee.obsolete, count: pushData11.pointee.obsolete_len)) + .to(equal([fakeHash3, fakeHash2, fakeHash1])) + + // Load some obsolete ones in just to check that they get immediately obsoleted + let fakeHash10: String = "fakehash10" + let cFakeHash10: [CChar] = fakeHash10.cArray.nullTerminated() + let fakeHash11: String = "fakehash11" + let cFakeHash11: [CChar] = fakeHash11.cArray.nullTerminated() + let fakeHash12: String = "fakehash12" + let cFakeHash12: [CChar] = fakeHash12.cArray.nullTerminated() + var mergeHashes3: [UnsafePointer?] = [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopy() + var mergeData3: [UnsafePointer?] = [ + UnsafePointer(pushData10.pointee.config), + UnsafePointer(pushData2.pointee.config), + UnsafePointer(pushData7.pointee.config), + UnsafePointer(pushData11.pointee.config) + ] + var mergeSize3: [Int] = [ + pushData10.pointee.config_len, + pushData2.pointee.config_len, + pushData7.pointee.config_len, + pushData11.pointee.config_len + ] + expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 4)).to(equal(4)) + expect(config_needs_dump(conf2)).to(beTrue()) + expect(config_needs_push(conf2)).to(beFalse()) + pushData2.deallocate() + pushData7.deallocate() + pushData10.deallocate() + pushData11.deallocate() + + let currentHashes5: UnsafeMutablePointer? = config_current_hashes(conf2) + expect([String](pointer: currentHashes5?.pointee.value, count: currentHashes5?.pointee.len)) + .to(equal([fakeHash4])) + currentHashes5?.deallocate() + + let pushData12: UnsafeMutablePointer = config_push(conf2) + expect(pushData12.pointee.seqno).to(equal(4)) + expect([String](pointer: pushData12.pointee.obsolete, count: pushData12.pointee.obsolete_len)) + .to(equal([fakeHash11, fakeHash12, fakeHash10, fakeHash3])) + pushData12.deallocate() + + for targetConf in [conf, conf2] { + // Iterate through and make sure we got everything we expected + var seen: [String] = [] + + var c1: ugroups_legacy_group_info = ugroups_legacy_group_info() + var c2: ugroups_community_info = ugroups_community_info() + let it: OpaquePointer = user_groups_iterator_new(targetConf) + + while !user_groups_iterator_done(it) { + if user_groups_it_is_legacy_group(it, &c1) { + var memberCount: Int = 0 + var adminCount: Int = 0 + ugroups_legacy_members_count(&c1, &memberCount, &adminCount) + + seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members") + } + else if user_groups_it_is_community(it, &c2) { + seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") + } + else { + seen.append("unknown") + } + + user_groups_iterator_advance(it) + } + + user_groups_iterator_free(it) + + expect(seen).to(equal([ + "community: http://jacksbeanstalk.org/r/fee", + "community: http://jacksbeanstalk.org/r/fi", + "community: http://jacksbeanstalk.org/r/fo", + "community: http://jacksbeanstalk.org/r/fum", + "legacy: Englishmen, 3 admins, 2 members" + ])) + } + } } } } diff --git a/SessionMessagingKitTests/LibSessionUtil/SessionUtilSpec.swift b/SessionMessagingKitTests/LibSessionUtil/SessionUtilSpec.swift index e8ac18129..f60ec1e79 100644 --- a/SessionMessagingKitTests/LibSessionUtil/SessionUtilSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/SessionUtilSpec.swift @@ -12,13 +12,12 @@ import Quick import Nimble class SessionUtilSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - SessionUtil describe("SessionUtil") { - // MARK: - Parsing URLs - + // MARK: -- when parsing a community url context("when parsing a community url") { + // MARK: ---- handles the example urls correctly it("handles the example urls correctly") { let validUrls: [String] = [ [ @@ -82,6 +81,7 @@ class SessionUtilSpec: QuickSpec { expect(processedPublicKeys).to(equal(expectedPublicKeys)) } + // MARK: ---- handles the r prefix if present it("handles the r prefix if present") { let info = SessionUtil.parseCommunity( url: [ @@ -95,6 +95,7 @@ class SessionUtilSpec: QuickSpec { expect(info?.publicKey).to(equal("658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c")) } + // MARK: ---- fails if no scheme is provided it("fails if no scheme is provided") { let info = SessionUtil.parseCommunity( url: [ @@ -108,6 +109,7 @@ class SessionUtilSpec: QuickSpec { expect(info?.publicKey).to(beNil()) } + // MARK: ---- fails if there is no room it("fails if there is no room") { let info = SessionUtil.parseCommunity( url: [ @@ -121,6 +123,7 @@ class SessionUtilSpec: QuickSpec { expect(info?.publicKey).to(beNil()) } + // MARK: ---- fails if there is no public key parameter it("fails if there is no public key parameter") { let info = SessionUtil.parseCommunity( url: "https://sessionopengroup.co/r/main" @@ -131,6 +134,7 @@ class SessionUtilSpec: QuickSpec { expect(info?.publicKey).to(beNil()) } + // MARK: ---- fails if the public key parameter is not 64 characters it("fails if the public key parameter is not 64 characters") { let info = SessionUtil.parseCommunity( url: [ @@ -144,6 +148,7 @@ class SessionUtilSpec: QuickSpec { expect(info?.publicKey).to(beNil()) } + // MARK: ---- fails if the public key parameter is not a hex string it("fails if the public key parameter is not a hex string") { let info = SessionUtil.parseCommunity( url: [ @@ -157,6 +162,7 @@ class SessionUtilSpec: QuickSpec { expect(info?.publicKey).to(beNil()) } + // MARK: ---- maintains the same TLS it("maintains the same TLS") { let server1 = SessionUtil.parseCommunity( url: [ @@ -175,6 +181,7 @@ class SessionUtilSpec: QuickSpec { expect(server2).to(equal("https://sessionopengroup.co")) } + // MARK: ---- maintains the same port it("maintains the same port") { let server1 = SessionUtil.parseCommunity( url: [ @@ -194,14 +201,15 @@ class SessionUtilSpec: QuickSpec { } } - // MARK: - Generating URLs - + // MARK: -- when generating a url context("when generating a url") { + // MARK: ---- generates the url correctly it("generates the url correctly") { expect(SessionUtil.communityUrlFor(server: "server", roomToken: "room", publicKey: "f8fec9b701000000ffffffff0400008000000000000000000000000000000000")) .to(equal("server/room?public_key=f8fec9b701000000ffffffff0400008000000000000000000000000000000000")) } + // MARK: ---- maintains the casing provided it("maintains the casing provided") { expect(SessionUtil.communityUrlFor(server: "SeRVer", roomToken: "RoOM", publicKey: "f8fec9b701000000ffffffff0400008000000000000000000000000000000000")) .to(equal("SeRVer/RoOM?public_key=f8fec9b701000000ffffffff0400008000000000000000000000000000000000")) diff --git a/SessionMessagingKitTests/LibSessionUtil/Utilities/LibSessionTypeConversionUtilitiesSpec.swift b/SessionMessagingKitTests/LibSessionUtil/Utilities/LibSessionTypeConversionUtilitiesSpec.swift index 5aeeff2b3..313372b5f 100644 --- a/SessionMessagingKitTests/LibSessionUtil/Utilities/LibSessionTypeConversionUtilitiesSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/Utilities/LibSessionTypeConversionUtilitiesSpec.swift @@ -9,16 +9,15 @@ import Nimble @testable import SessionMessagingKit class LibSessionTypeConversionUtilitiesSpec: QuickSpec { - // MARK: - Spec - - override func spec() { - // MARK: - String - + override class func spec() { + // MARK: - a String describe("a String") { + // MARK: -- can convert to a cArray it("can convert to a cArray") { expect("Test123".cArray).to(equal([84, 101, 115, 116, 49, 50, 51])) } + // MARK: -- can contain emoji it("can contain emoji") { let original: String = "Hi 👋" let libSessionVal: (CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar) = original.toLibSession() @@ -27,7 +26,9 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal(original)) } + // MARK: -- when initialised with a pointer and length context("when initialised with a pointer and length") { + // MARK: ---- returns null when given a null pointer it("returns null when given a null pointer") { let test: [CChar] = [84, 101, 115, 116] let result = test.withUnsafeBufferPointer { ptr in @@ -37,6 +38,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(beNil()) } + // MARK: ---- returns a truncated string when given an incorrect length it("returns a truncated string when given an incorrect length") { let test: [CChar] = [84, 101, 115, 116] let result = test.withUnsafeBufferPointer { ptr in @@ -46,6 +48,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal("Te")) } + // MARK: ---- returns a string when valid it("returns a string when valid") { let test: [CChar] = [84, 101, 115, 116] let result = test.withUnsafeBufferPointer { ptr in @@ -56,7 +59,9 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { } } + // MARK: -- when initialised with a libSession value context("when initialised with a libSession value") { + // MARK: ---- returns a string when valid and has no fixed length it("returns a string when valid and has no fixed length") { let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 115, 116, 0) let result = String(libSessionVal: value, fixedLength: .none) @@ -64,6 +69,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal("Test")) } + // MARK: ---- returns a string when valid and has a fixed length it("returns a string when valid and has a fixed length") { let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 0, 115, 116) let result = String(libSessionVal: value, fixedLength: 5) @@ -71,6 +77,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal("Te\0st")) } + // MARK: ---- truncates at the first null termination character when fixed length is none it("truncates at the first null termination character when fixed length is none") { let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 0, 115, 116) let result = String(libSessionVal: value, fixedLength: .none) @@ -78,6 +85,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal("Te")) } + // MARK: ---- parses successfully if there is no null termination character and there is no fixed length it("parses successfully if there is no null termination character and there is no fixed length") { let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 115, 116, 84) let result = String(libSessionVal: value, fixedLength: .none) @@ -85,6 +93,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal("TestT")) } + // MARK: ---- returns an empty string when given a value only containing null termination characters with a fixed length it("returns an empty string when given a value only containing null termination characters with a fixed length") { let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0) let result = String(libSessionVal: value, fixedLength: 5) @@ -92,6 +101,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal("")) } + // MARK: ---- defaults the fixed length value to none it("defaults the fixed length value to none") { let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 0, 0, 0) let result = String(libSessionVal: value) @@ -99,6 +109,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal("Te")) } + // MARK: ---- returns an empty string when null and not set to return null it("returns an empty string when null and not set to return null") { let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0) let result = String(libSessionVal: value, nullIfEmpty: false) @@ -106,6 +117,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal("")) } + // MARK: ---- returns null when specified and empty it("returns null when specified and empty") { let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0) let result = String(libSessionVal: value, nullIfEmpty: true) @@ -113,6 +125,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(beNil()) } + // MARK: ---- defaults the null if empty flag to false it("defaults the null if empty flag to false") { let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0) let result = String(libSessionVal: value) @@ -121,7 +134,9 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { } } + // MARK: -- when converting to a libSession value context("when converting to a libSession value") { + // MARK: ---- succeeeds with a valid value it("succeeeds with a valid value") { let result: (CChar, CChar, CChar, CChar, CChar) = "Test".toLibSession() expect(result.0).to(equal(84)) @@ -131,6 +146,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result.4).to(equal(0)) } + // MARK: ---- truncates when too long it("truncates when too long") { let result: (CChar, CChar, CChar, CChar, CChar) = "TestTest".toLibSession() expect(result.0).to(equal(84)) @@ -140,7 +156,9 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result.4).to(equal(84)) } + // MARK: ---- when optional context("when optional") { + // MARK: ------ returns empty when null context("returns empty when null") { let value: String? = nil let result: (CChar, CChar, CChar, CChar, CChar) = value.toLibSession() @@ -152,6 +170,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result.4).to(equal(0)) } + // MARK: ------ returns a libSession value when not null context("returns a libSession value when not null") { let value: String? = "Test" let result: (CChar, CChar, CChar, CChar, CChar) = value.toLibSession() @@ -167,13 +186,15 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { } // MARK: - Data - describe("Data") { + // MARK: -- can convert to a cArray it("can convert to a cArray") { expect(Data([1, 2, 3]).cArray).to(equal([1, 2, 3])) } + // MARK: -- when initialised with a libSession value context("when initialised with a libSession value") { + // MARK: ---- returns truncated data when given the wrong length it("returns truncated data when given the wrong length") { let value: (UInt8, UInt8, UInt8, UInt8, UInt8) = (1, 2, 3, 4, 5) let result = Data(libSessionVal: value, count: 2) @@ -181,6 +202,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal(Data([1, 2]))) } + // MARK: ---- returns data when valid it("returns data when valid") { let value: (UInt8, UInt8, UInt8, UInt8, UInt8) = (1, 2, 3, 4, 5) let result = Data(libSessionVal: value, count: 5) @@ -188,6 +210,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal(Data([1, 2, 3, 4, 5]))) } + // MARK: ---- returns data when all bytes are zero and nullIfEmpty is false it("returns data when all bytes are zero and nullIfEmpty is false") { let value: (UInt8, UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0, 0) let result = Data(libSessionVal: value, count: 5, nullIfEmpty: false) @@ -195,6 +218,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal(Data([0, 0, 0, 0, 0]))) } + // MARK: ---- returns null when all bytes are zero and nullIfEmpty is true it("returns null when all bytes are zero and nullIfEmpty is true") { let value: (UInt8, UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0, 0) let result = Data(libSessionVal: value, count: 5, nullIfEmpty: true) @@ -203,7 +227,9 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { } } + // MARK: -- when converting to a libSession value context("when converting to a libSession value") { + // MARK: ---- succeeeds with a valid value it("succeeeds with a valid value") { let result: (Int8, Int8, Int8, Int8, Int8) = Data([1, 2, 3, 4, 5]).toLibSession() expect(result.0).to(equal(1)) @@ -213,6 +239,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result.4).to(equal(5)) } + // MARK: ---- truncates when too long it("truncates when too long") { let result: (Int8, Int8, Int8, Int8, Int8) = Data([1, 2, 3, 4, 1, 2, 3, 4]).toLibSession() expect(result.0).to(equal(1)) @@ -222,6 +249,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result.4).to(equal(1)) } + // MARK: ---- fills with empty data when too short context("fills with empty data when too short") { let value: Data? = Data([1, 2, 3]) let result: (Int8, Int8, Int8, Int8, Int8) = value.toLibSession() @@ -233,7 +261,9 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result.4).to(equal(0)) } + // MARK: ---- when optional context("when optional") { + // MARK: ------ returns null when null context("returns null when null") { let value: Data? = nil let result: (Int8, Int8, Int8, Int8, Int8) = value.toLibSession() @@ -245,6 +275,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result.4).to(equal(0)) } + // MARK: ------ returns a libSession value when not null context("returns a libSession value when not null") { let value: Data? = Data([1, 2, 3, 4, 5]) let result: (Int8, Int8, Int8, Int8, Int8) = value.toLibSession() @@ -259,10 +290,11 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { } } - // MARK: - Array - + // MARK: - an Array describe("an Array") { + // MARK: -- when initialised with a 2D C array context("when initialised with a 2D C array") { + // MARK: ---- returns the correct array it("returns the correct array") { var test: [CChar] = ( "Test1".cArray.nullTerminated() + @@ -278,6 +310,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal(["Test1", "Test2", "Test3AndExtra"])) } + // MARK: ---- returns an empty array if given one it("returns an empty array if given one") { var test = [CChar]() let result = test.withUnsafeMutableBufferPointer { ptr in @@ -289,6 +322,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal([])) } + // MARK: ---- handles empty strings without issues it("handles empty strings without issues") { var test: [CChar] = ( "Test1".cArray.nullTerminated() + @@ -304,10 +338,12 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(equal(["Test1", "", "Test2"])) } + // MARK: ---- returns null when given a null pointer it("returns null when given a null pointer") { expect([String](pointer: nil, count: 5)).to(beNil()) } + // MARK: ---- returns null when given a null count it("returns null when given a null count") { var test: [CChar] = "Test1".cArray.nullTerminated() let result = test.withUnsafeMutableBufferPointer { ptr in @@ -319,19 +355,23 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { expect(result).to(beNil()) } + // MARK: ---- returns the default value if given null values it("returns the default value if given null values") { expect([String](pointer: nil, count: 5, defaultValue: ["Test"])) .to(equal(["Test"])) } } + // MARK: -- when adding a null terminated character context("when adding a null terminated character") { + // MARK: ---- adds a null termination character when not present it("adds a null termination character when not present") { let value: [CChar] = [1, 2, 3, 4, 5] expect(value.nullTerminated()).to(equal([1, 2, 3, 4, 5, 0])) } + // MARK: ---- adds nothing when already present it("adds nothing when already present") { let value: [CChar] = [1, 2, 3, 4, 0] diff --git a/SessionMessagingKitTests/Open Groups/Models/BatchRequestInfoSpec.swift b/SessionMessagingKitTests/Open Groups/Models/BatchRequestInfoSpec.swift index 1d8508abe..258c2c1a9 100644 --- a/SessionMessagingKitTests/Open Groups/Models/BatchRequestInfoSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/BatchRequestInfoSpec.swift @@ -9,22 +9,19 @@ import Quick import Nimble @testable import SessionMessagingKit -import AVFoundation class BatchRequestInfoSpec: QuickSpec { - struct TestType: Codable, Equatable { - let stringValue: String - } - - // MARK: - Spec - - override func spec() { - // MARK: - BatchRequest.Child + override class func spec() { + // MARK: Configuration + + @TestState var request: OpenGroupAPI.BatchRequest! + + // MARK: - a BatchRequest.Child describe("a BatchRequest.Child") { - var request: OpenGroupAPI.BatchRequest! - + // MARK: -- when encoding context("when encoding") { + // MARK: ---- successfully encodes a string body it("successfully encodes a string body") { request = OpenGroupAPI.BatchRequest( requests: [ @@ -53,6 +50,7 @@ class BatchRequestInfoSpec: QuickSpec { expect(requestJson?.first?["b64"] as? String).to(equal("testBody")) } + // MARK: ---- successfully encodes a byte body it("successfully encodes a byte body") { request = OpenGroupAPI.BatchRequest( requests: [ @@ -81,6 +79,7 @@ class BatchRequestInfoSpec: QuickSpec { expect(requestJson?.first?["bytes"] as? [Int]).to(equal([1, 2, 3])) } + // MARK: ---- successfully encodes a JSON body it("successfully encodes a JSON body") { request = OpenGroupAPI.BatchRequest( requests: [ @@ -109,6 +108,7 @@ class BatchRequestInfoSpec: QuickSpec { expect(requestJson?.first?["json"] as? [String: String]).to(equal(["stringValue": "testValue"])) } + // MARK: ---- strips authentication headers it("strips authentication headers") { let httpRequest: Request = Request( method: .get, @@ -149,6 +149,7 @@ class BatchRequestInfoSpec: QuickSpec { } } + // MARK: -- does not strip non authentication headers it("does not strip non authentication headers") { let httpRequest: Request = Request( method: .get, @@ -185,3 +186,9 @@ class BatchRequestInfoSpec: QuickSpec { } } } + +// MARK: - Test Types + +fileprivate struct TestType: Codable, Equatable { + let stringValue: String +} diff --git a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift b/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift index 220c9bad2..0fc98cf1a 100644 --- a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift @@ -8,11 +8,12 @@ import Nimble @testable import SessionMessagingKit class CapabilitiesSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - Capabilities describe("Capabilities") { + // MARK: -- when initializing context("when initializing") { + // MARK: ---- assigns values correctly it("assigns values correctly") { let capabilities: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities( capabilities: [.sogs], @@ -34,8 +35,11 @@ class CapabilitiesSpec: QuickSpec { } } + // MARK: - a Capability describe("a Capability") { + // MARK: -- when initializing context("when initializing") { + // MARK: ---- succeeeds with a valid case it("succeeeds with a valid case") { let capability: Capability.Variant = Capability.Variant( from: "sogs" @@ -44,6 +48,7 @@ class CapabilitiesSpec: QuickSpec { expect(capability).to(equal(.sogs)) } + // MARK: ---- wraps an unknown value in the unsupported case it("wraps an unknown value in the unsupported case") { let capability: Capability.Variant = Capability.Variant( from: "test" @@ -53,18 +58,23 @@ class CapabilitiesSpec: QuickSpec { } } + // MARK: -- when accessing the rawValue context("when accessing the rawValue") { + // MARK: ---- provides known cases exactly it("provides known cases exactly") { expect(Capability.Variant.sogs.rawValue).to(equal("sogs")) expect(Capability.Variant.blind.rawValue).to(equal("blind")) } + // MARK: ---- provides the wrapped value for unsupported cases it("provides the wrapped value for unsupported cases") { expect(Capability.Variant.unsupported("test").rawValue).to(equal("test")) } } + // MARK: -- when Decoding context("when Decoding") { + // MARK: ---- decodes known cases exactly it("decodes known cases exactly") { expect( try? JSONDecoder().decode( @@ -82,6 +92,7 @@ class CapabilitiesSpec: QuickSpec { .to(equal(.blind)) } + // MARK: ---- decodes unknown cases into the unsupported case it("decodes unknown cases into the unsupported case") { expect( try? JSONDecoder().decode( diff --git a/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift b/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift index ea835122f..3ad7cae22 100644 --- a/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift @@ -8,11 +8,12 @@ import Nimble @testable import SessionMessagingKit class OpenGroupSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - an Open Group describe("an Open Group") { + // MARK: -- when initializing context("when initializing") { + // MARK: ---- generates the id it("generates the id") { let openGroup: OpenGroup = OpenGroup( server: "server", @@ -34,7 +35,9 @@ class OpenGroupSpec: QuickSpec { } } + // MARK: -- when describing context("when describing") { + // MARK: ---- includes relevant information it("includes relevant information") { let openGroup: OpenGroup = OpenGroup( server: "server", @@ -57,7 +60,9 @@ class OpenGroupSpec: QuickSpec { } } + // MARK: -- when describing in debug context("when describing in debug") { + // MARK: ---- includes relevant information it("includes relevant information") { let openGroup: OpenGroup = OpenGroup( server: "server", @@ -80,15 +85,19 @@ class OpenGroupSpec: QuickSpec { } } + // MARK: -- when generating an id context("when generating an id") { + // MARK: ---- generates correctly it("generates correctly") { expect(OpenGroup.idFor(roomToken: "room", server: "server")).to(equal("server.room")) } + // MARK: ---- converts the server to lowercase it("converts the server to lowercase") { expect(OpenGroup.idFor(roomToken: "room", server: "SeRVeR")).to(equal("server.room")) } + // MARK: ---- maintains the casing of the roomToken it("maintains the casing of the roomToken") { expect(OpenGroup.idFor(roomToken: "RoOM", server: "server")).to(equal("server.RoOM")) } diff --git a/SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift b/SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift index 4d5156ef3..386f2f02b 100644 --- a/SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift @@ -8,11 +8,12 @@ import Nimble @testable import SessionMessagingKit class RoomPollInfoSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - a RoomPollInfo describe("a RoomPollInfo") { + // MARK: -- when initializing with a room context("when initializing with a room") { + // MARK: ---- copies all the relevant values across it("copies all the relevant values across") { let room: OpenGroupAPI.Room = OpenGroupAPI.Room( token: "testToken", @@ -60,7 +61,9 @@ class RoomPollInfoSpec: QuickSpec { } } + // MARK: -- when decoding context("when decoding") { + // MARK: ---- defaults admin and moderator values to false if omitted it("defaults admin and moderator values to false if omitted") { let roomPollInfoJson: String = """ { @@ -87,6 +90,7 @@ class RoomPollInfoSpec: QuickSpec { expect(result.globalModerator).to(beFalse()) } + // MARK: ---- sets the admin and moderator values when provided it("sets the admin and moderator values when provided") { let roomPollInfoJson: String = """ { diff --git a/SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift b/SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift index 16a3ab84b..2fd43d679 100644 --- a/SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift @@ -8,11 +8,12 @@ import Nimble @testable import SessionMessagingKit class RoomSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - a Room describe("a Room") { + // MARK: -- when decoding context("when decoding") { + // MARK: ---- defaults admin and moderator values to false if omitted it("defaults admin and moderator values to false if omitted") { let roomJson: String = """ { @@ -52,6 +53,7 @@ class RoomSpec: QuickSpec { expect(result.globalModerator).to(beFalse()) } + // MARK: ---- sets the admin and moderator values when provided it("sets the admin and moderator values when provided") { let roomJson: String = """ { diff --git a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift index 040785fe8..a5fbf423b 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift @@ -9,44 +9,38 @@ import SessionUtilitiesKit @testable import SessionMessagingKit class SOGSMessageSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: Configuration + + @TestState var messageJson: String! = """ + { + "id": 123, + "session_id": "05\(TestConstants.publicKey)", + "posted": 234, + "seqno": 345, + "whisper": false, + "whisper_mods": false, + + "data": "VGVzdERhdGE=", + "signature": "VGVzdFNpZ25hdHVyZQ==" + } + """ + @TestState var messageData: Data! = messageJson.data(using: .utf8)! + @TestState var mockCrypto: MockCrypto! = MockCrypto() + @TestState var dependencies: Dependencies! = Dependencies( + crypto: mockCrypto + ) + @TestState var decoder: JSONDecoder! = { + let result = JSONDecoder() + result.userInfo = [ Dependencies.userInfoKey: dependencies as Any ] + return result + }() + + // MARK: - a SOGSMessage describe("a SOGSMessage") { - var messageJson: String! - var messageData: Data! - var decoder: JSONDecoder! - var mockCrypto: MockCrypto! - var dependencies: Dependencies! - - beforeEach { - messageJson = """ - { - "id": 123, - "session_id": "05\(TestConstants.publicKey)", - "posted": 234, - "seqno": 345, - "whisper": false, - "whisper_mods": false, - - "data": "VGVzdERhdGE=", - "signature": "VGVzdFNpZ25hdHVyZQ==" - } - """ - messageData = messageJson.data(using: .utf8)! - mockCrypto = MockCrypto() - dependencies = Dependencies( - crypto: mockCrypto - ) - decoder = JSONDecoder() - decoder.userInfo = [ Dependencies.userInfoKey: dependencies as Any ] - } - - afterEach { - mockCrypto = nil - } - + // MARK: -- when decoding context("when decoding") { + // MARK: ---- defaults the whisper values to false it("defaults the whisper values to false") { messageJson = """ { @@ -63,7 +57,9 @@ class SOGSMessageSpec: QuickSpec { expect(result?.whisperMods).to(beFalse()) } + // MARK: ---- and there is no content context("and there is no content") { + // MARK: ------ does not need a sender it("does not need a sender") { messageJson = """ { @@ -84,7 +80,9 @@ class SOGSMessageSpec: QuickSpec { } } + // MARK: ---- and there is content context("and there is content") { + // MARK: ------ errors if there is no sender it("errors if there is no sender") { messageJson = """ { @@ -106,6 +104,7 @@ class SOGSMessageSpec: QuickSpec { .to(throwError(HTTPError.parsingFailed)) } + // MARK: ------ errors if the data is not a base64 encoded string it("errors if the data is not a base64 encoded string") { messageJson = """ { @@ -128,6 +127,7 @@ class SOGSMessageSpec: QuickSpec { .to(throwError(HTTPError.parsingFailed)) } + // MARK: ------ errors if the signature is not a base64 encoded string it("errors if the signature is not a base64 encoded string") { messageJson = """ { @@ -150,6 +150,7 @@ class SOGSMessageSpec: QuickSpec { .to(throwError(HTTPError.parsingFailed)) } + // MARK: ------ errors if the dependencies are not provided to the JSONDecoder it("errors if the dependencies are not provided to the JSONDecoder") { decoder = JSONDecoder() @@ -159,6 +160,7 @@ class SOGSMessageSpec: QuickSpec { .to(throwError(HTTPError.parsingFailed)) } + // MARK: ------ errors if the session_id value is not valid it("errors if the session_id value is not valid") { messageJson = """ { @@ -181,7 +183,7 @@ class SOGSMessageSpec: QuickSpec { .to(throwError(HTTPError.parsingFailed)) } - + // MARK: ------ that is blinded context("that is blinded") { beforeEach { messageJson = """ @@ -200,6 +202,7 @@ class SOGSMessageSpec: QuickSpec { messageData = messageJson.data(using: .utf8)! } + // MARK: -------- succeeds if it succeeds verification it("succeeds if it succeeds verification") { mockCrypto .when { @@ -213,6 +216,7 @@ class SOGSMessageSpec: QuickSpec { .toNot(beNil()) } + // MARK: -------- provides the correct values as parameters it("provides the correct values as parameters") { mockCrypto .when { @@ -234,6 +238,7 @@ class SOGSMessageSpec: QuickSpec { }) } + // MARK: -------- throws if it fails verification it("throws if it fails verification") { mockCrypto .when { @@ -248,7 +253,9 @@ class SOGSMessageSpec: QuickSpec { } } + // MARK: ------ that is unblinded context("that is unblinded") { + // MARK: -------- succeeds if it succeeds verification it("succeeds if it succeeds verification") { mockCrypto .when { $0.verify(.signatureEd25519(any(), publicKey: any(), data: any())) } @@ -260,6 +267,7 @@ class SOGSMessageSpec: QuickSpec { .toNot(beNil()) } + // MARK: -------- provides the correct values as parameters it("provides the correct values as parameters") { mockCrypto .when { $0.verify(.signatureEd25519(any(), publicKey: any(), data: any())) } @@ -279,6 +287,7 @@ class SOGSMessageSpec: QuickSpec { }) } + // MARK: -------- throws if it fails verification it("throws if it fails verification") { mockCrypto .when { $0.verify(.signatureEd25519(any(), publicKey: any(), data: any())) } diff --git a/SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift b/SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift index 228176a15..27e96dd20 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift @@ -8,11 +8,12 @@ import Nimble @testable import SessionMessagingKit class SendDirectMessageRequestSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - a SendDirectMessageRequest describe("a SendDirectMessageRequest") { + // MARK: -- when encoding context("when encoding") { + // MARK: ---- encodes the data as a base64 string it("encodes the data as a base64 string") { let request: OpenGroupAPI.SendDirectMessageRequest = OpenGroupAPI.SendDirectMessageRequest( message: "TestData".data(using: .utf8)! diff --git a/SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift b/SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift index 7fd3554a3..ec0c15d38 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift @@ -8,11 +8,12 @@ import Nimble @testable import SessionMessagingKit class SendMessageRequestSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - a SendMessageRequest describe("a SendMessageRequest") { + // MARK: -- when initializing context("when initializing") { + // MARK: ---- defaults the optional values to nil it("defaults the optional values to nil") { let request: OpenGroupAPI.SendMessageRequest = OpenGroupAPI.SendMessageRequest( data: "TestData".data(using: .utf8)!, @@ -25,7 +26,9 @@ class SendMessageRequestSpec: QuickSpec { } } + // MARK: -- when encoding context("when encoding") { + // MARK: ---- encodes the data as a base64 string it("encodes the data as a base64 string") { let request: OpenGroupAPI.SendMessageRequest = OpenGroupAPI.SendMessageRequest( data: "TestData".data(using: .utf8)!, @@ -41,6 +44,7 @@ class SendMessageRequestSpec: QuickSpec { expect(requestDataString).to(contain("VGVzdERhdGE=")) } + // MARK: ---- encodes the signature as a base64 string it("encodes the signature as a base64 string") { let request: OpenGroupAPI.SendMessageRequest = OpenGroupAPI.SendMessageRequest( data: "TestData".data(using: .utf8)!, diff --git a/SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift b/SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift index f63b2e16c..106bd04c5 100644 --- a/SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift @@ -8,11 +8,12 @@ import Nimble @testable import SessionMessagingKit class UpdateMessageRequestSpec: QuickSpec { - // MARK: - Spec - - override func spec() { - describe("a UpdateMessageRequest") { + override class func spec() { + // MARK: - an UpdateMessageRequest + describe("an UpdateMessageRequest") { + // MARK: -- when encoding context("when encoding") { + // MARK: ---- encodes the data as a base64 string it("encodes the data as a base64 string") { let request: OpenGroupAPI.UpdateMessageRequest = OpenGroupAPI.UpdateMessageRequest( data: "TestData".data(using: .utf8)!, @@ -26,6 +27,7 @@ class UpdateMessageRequestSpec: QuickSpec { expect(requestDataString).to(contain("VGVzdERhdGE=")) } + // MARK: ---- encodes the signature as a base64 string it("encodes the signature as a base64 string") { let request: OpenGroupAPI.UpdateMessageRequest = OpenGroupAPI.UpdateMessageRequest( data: "TestData".data(using: .utf8)!, diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index 8586c99d1..0470715e5 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -13,66 +13,49 @@ import Nimble @testable import SessionMessagingKit class OpenGroupAPISpec: QuickSpec { - // MARK: - Spec - - override func spec() { - var mockStorage: Storage! - var mockNetwork: MockNetwork! - var mockCrypto: MockCrypto! - var dependencies: Dependencies! - var disposables: [AnyCancellable] = [] + override class func spec() { + // MARK: Configuration - var error: Error? - - describe("an OpenGroupAPI") { - // MARK: - Configuration - - beforeEach { - mockStorage = SynchronousStorage( - customWriter: try! DatabaseQueue(), - customMigrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ] - ) - mockNetwork = MockNetwork() - mockCrypto = MockCrypto() - dependencies = Dependencies( - storage: mockStorage, - network: mockNetwork, - crypto: mockCrypto, - dateNow: Date(timeIntervalSince1970: 1234567890) - ) + @TestState var mockStorage: Storage! = SynchronousStorage( + customWriter: try! DatabaseQueue(), + customMigrationTargets: [ + SNUtilitiesKit.self, + SNMessagingKit.self + ], + initialData: { db in + try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data.data(fromHex: TestConstants.privateKey)!).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: TestConstants.edPublicKey)!).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data.data(fromHex: TestConstants.edSecretKey)!).insert(db) - mockStorage.write { db in - try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data.data(fromHex: TestConstants.privateKey)!).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: TestConstants.edPublicKey)!).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data.data(fromHex: TestConstants.edSecretKey)!).insert(db) - - try OpenGroup( - server: "testServer", - roomToken: "testRoom", - publicKey: TestConstants.publicKey, - isActive: true, - name: "Test", - roomDescription: nil, - imageId: nil, - userCount: 0, - infoUpdates: 0, - sequenceNumber: 0, - inboxLatestMessageId: 0, - outboxLatestMessageId: 0 - ).insert(db) - try Capability(openGroupServer: "testserver", variant: .sogs, isMissing: false).insert(db) - } - - mockCrypto + try OpenGroup( + server: "testServer", + roomToken: "testRoom", + publicKey: TestConstants.publicKey, + isActive: true, + name: "Test", + roomDescription: nil, + imageId: nil, + userCount: 0, + infoUpdates: 0, + sequenceNumber: 0, + inboxLatestMessageId: 0, + outboxLatestMessageId: 0 + ).insert(db) + try Capability(openGroupServer: "testserver", variant: .sogs, isMissing: false).insert(db) + } + ) + @TestState var mockNetwork: MockNetwork! = MockNetwork() + @TestState var mockCrypto: MockCrypto! = MockCrypto( + initialSetup: { crypto in + crypto .when { try $0.perform(.hash(message: anyArray(), outputLength: any())) } .thenReturn([]) - mockCrypto - .when { [dependencies = dependencies!] crypto in - crypto.generate(.blindedKeyPair(serverPublicKey: any(), edKeyPair: any(), using: dependencies)) + crypto + .when { crypto in + crypto.generate( + .blindedKeyPair(serverPublicKey: any(), edKeyPair: any(), using: any()) + ) } .thenReturn( KeyPair( @@ -80,7 +63,7 @@ class OpenGroupAPISpec: QuickSpec { secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ) ) - mockCrypto + crypto .when { try $0.perform( .sogsSignature( @@ -92,35 +75,34 @@ class OpenGroupAPISpec: QuickSpec { ) } .thenReturn("TestSogsSignature".bytes) - mockCrypto + crypto .when { try $0.perform(.signature(message: anyArray(), secretKey: anyArray())) } .thenReturn("TestSignature".bytes) - mockCrypto + crypto .when { try $0.perform(.signEd25519(data: anyArray(), keyPair: any())) } .thenReturn("TestStandardSignature".bytes) - mockCrypto + crypto .when { try $0.perform(.generateNonce16()) } .thenReturn(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!.bytes) - mockCrypto + crypto .when { try $0.perform(.generateNonce24()) } .thenReturn(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes) } - - afterEach { - disposables.forEach { $0.cancel() } - - mockStorage = nil - mockNetwork = nil - mockCrypto = nil - dependencies = nil - disposables = [] - - error = nil - } - - // MARK: - when preparing a poll request + ) + @TestState var dependencies: Dependencies! = Dependencies( + storage: mockStorage, + network: mockNetwork, + crypto: mockCrypto, + dateNow: Date(timeIntervalSince1970: 1234567890) + ) + @TestState var disposables: [AnyCancellable]! = [] + @TestState var error: Error? + + // MARK: - an OpenGroupAPI + describe("an OpenGroupAPI") { + // MARK: -- when preparing a poll request context("when preparing a poll request") { - // MARK: -- generates the correct request + // MARK: ---- generates the correct request it("generates the correct request") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedPoll( @@ -140,7 +122,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.batchEndpoints[test: 2]).to(equal(.roomMessagesRecent("testRoom"))) } - // MARK: -- retrieves recent messages if there was no last message + // MARK: ---- retrieves recent messages if there was no last message it("retrieves recent messages if there was no last message") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedPoll( @@ -155,7 +137,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.batchEndpoints[test: 2]).to(equal(.roomMessagesRecent("testRoom"))) } - // MARK: -- retrieves recent messages if there was a last message and it has not performed the initial poll and the last message was too long ago + // MARK: ---- retrieves recent messages if there was a last message and it has not performed the initial poll and the last message was too long ago it("retrieves recent messages if there was a last message and it has not performed the initial poll and the last message was too long ago") { mockStorage.write { db in try OpenGroup @@ -175,7 +157,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.batchEndpoints[test: 2]).to(equal(.roomMessagesRecent("testRoom"))) } - // MARK: -- retrieves recent messages if there was a last message and it has performed an initial poll but it was not too long ago + // MARK: ---- retrieves recent messages if there was a last message and it has performed an initial poll but it was not too long ago it("retrieves recent messages if there was a last message and it has performed an initial poll but it was not too long ago") { mockStorage.write { db in try OpenGroup @@ -196,7 +178,7 @@ class OpenGroupAPISpec: QuickSpec { .to(equal(.roomMessagesSince("testRoom", seqNo: 122))) } - // MARK: -- retrieves recent messages if there was a last message and there has already been a poll this session + // MARK: ---- retrieves recent messages if there was a last message and there has already been a poll this session it("retrieves recent messages if there was a last message and there has already been a poll this session") { mockStorage.write { db in try OpenGroup @@ -217,7 +199,7 @@ class OpenGroupAPISpec: QuickSpec { .to(equal(.roomMessagesSince("testRoom", seqNo: 123))) } - // MARK: -- when unblinded + // MARK: ---- when unblinded context("when unblinded") { beforeEach { mockStorage.write { db in @@ -226,7 +208,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: ---- does not call the inbox and outbox endpoints + // MARK: ------ does not call the inbox and outbox endpoints it("does not call the inbox and outbox endpoints") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedPoll( @@ -243,7 +225,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: -- when blinded and checking for message requests + // MARK: ---- when blinded and checking for message requests context("when blinded and checking for message requests") { beforeEach { mockStorage.write { db in @@ -255,7 +237,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: ---- includes the inbox and outbox endpoints + // MARK: ------ includes the inbox and outbox endpoints it("includes the inbox and outbox endpoints") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedPoll( @@ -271,7 +253,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.batchEndpoints).to(contain(.outbox)) } - // MARK: ---- retrieves recent inbox messages if there was no last message + // MARK: ------ retrieves recent inbox messages if there was no last message it("retrieves recent inbox messages if there was no last message") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedPoll( @@ -286,7 +268,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.batchEndpoints).to(contain(.inbox)) } - // MARK: ---- retrieves inbox messages since the last message if there was one + // MARK: ------ retrieves inbox messages since the last message if there was one it("retrieves inbox messages since the last message if there was one") { mockStorage.write { db in try OpenGroup @@ -306,7 +288,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.batchEndpoints).to(contain(.inboxSince(id: 124))) } - // MARK: ---- retrieves recent outbox messages if there was no last message + // MARK: ------ retrieves recent outbox messages if there was no last message it("retrieves recent outbox messages if there was no last message") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedPoll( @@ -321,7 +303,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.batchEndpoints).to(contain(.outbox)) } - // MARK: ---- retrieves outbox messages since the last message if there was one + // MARK: ------ retrieves outbox messages since the last message if there was one it("retrieves outbox messages since the last message if there was one") { mockStorage.write { db in try OpenGroup @@ -342,7 +324,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: -- when blinded and not checking for message requests + // MARK: ---- when blinded and not checking for message requests context("when blinded and not checking for message requests") { beforeEach { mockStorage.write { db in @@ -354,7 +336,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: ---- includes the inbox and outbox endpoints + // MARK: ------ includes the inbox and outbox endpoints it("does not include the inbox endpoint") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedPoll( @@ -369,7 +351,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.batchEndpoints).toNot(contain(.inbox)) } - // MARK: ---- does not retrieve recent inbox messages if there was no last message + // MARK: ------ does not retrieve recent inbox messages if there was no last message it("does not retrieve recent inbox messages if there was no last message") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedPoll( @@ -384,7 +366,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.batchEndpoints).toNot(contain(.inbox)) } - // MARK: ---- does not retrieve inbox messages since the last message if there was one + // MARK: ------ does not retrieve inbox messages since the last message if there was one it("does not retrieve inbox messages since the last message if there was one") { mockStorage.write { db in try OpenGroup @@ -406,9 +388,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a capabilities request + // MARK: -- when preparing a capabilities request context("when preparing a capabilities request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request and handles the response correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedCapabilities( @@ -423,10 +405,10 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a rooms request + // MARK: -- when preparing a rooms request context("when preparing a rooms request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.Room]>? = mockStorage.read { db in try OpenGroupAPI.preparedRooms( @@ -441,9 +423,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a capabilitiesAndRoom request + // MARK: -- when preparing a capabilitiesAndRoom request context("when preparing a capabilitiesAndRoom request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedCapabilitiesAndRoom( @@ -462,7 +444,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.request.httpMethod).to(equal("POST")) } - // MARK: -- processes a valid response correctly + // MARK: ---- processes a valid response correctly it("processes a valid response correctly") { mockNetwork .when { $0.send(.onionRequest(any(), to: any(), with: any())) } @@ -488,10 +470,10 @@ class OpenGroupAPISpec: QuickSpec { expect(error).to(beNil()) } - // MARK: -- and given an invalid response + // MARK: ---- and given an invalid response context("and given an invalid response") { - // MARK: ---- errors when not given a room response + // MARK: ------ errors when not given a room response it("errors when not given a room response") { mockNetwork .when { $0.send(.onionRequest(any(), to: any(), with: any())) } @@ -517,7 +499,7 @@ class OpenGroupAPISpec: QuickSpec { expect(response).to(beNil()) } - // MARK: ---- errors when not given a capabilities response + // MARK: ------ errors when not given a capabilities response it("errors when not given a capabilities response") { mockNetwork .when { $0.send(.onionRequest(any(), to: any(), with: any())) } @@ -545,9 +527,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a capabilitiesAndRooms request + // MARK: -- when preparing a capabilitiesAndRooms request context("when preparing a capabilitiesAndRooms request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedCapabilitiesAndRooms( @@ -565,7 +547,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.request.httpMethod).to(equal("POST")) } - // MARK: -- processes a valid response correctly + // MARK: ---- processes a valid response correctly it("processes a valid response correctly") { mockNetwork .when { $0.send(.onionRequest(any(), to: any(), with: any())) } @@ -590,10 +572,10 @@ class OpenGroupAPISpec: QuickSpec { expect(error).to(beNil()) } - // MARK: -- and given an invalid response + // MARK: ---- and given an invalid response context("and given an invalid response") { - // MARK: ---- errors when not given a room response + // MARK: ------ errors when not given a room response it("errors when not given a room response") { mockNetwork .when { $0.send(.onionRequest(any(), to: any(), with: any())) } @@ -618,7 +600,7 @@ class OpenGroupAPISpec: QuickSpec { expect(response).to(beNil()) } - // MARK: ---- errors when not given a capabilities response + // MARK: ------ errors when not given a capabilities response it("errors when not given a capabilities response") { mockNetwork .when { $0.send(.onionRequest(any(), to: any(), with: any())) } @@ -645,9 +627,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a send message request + // MARK: -- when preparing a send message request context("when preparing a send message request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedSend( @@ -666,7 +648,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.request.httpMethod).to(equal("POST")) } - // MARK: -- when unblinded + // MARK: ---- when unblinded context("when unblinded") { beforeEach { mockStorage.write { db in @@ -675,7 +657,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: ---- signs the message correctly + // MARK: ------ signs the message correctly it("signs the message correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedSend( @@ -696,7 +678,7 @@ class OpenGroupAPISpec: QuickSpec { expect(requestBody?.signature).to(equal("TestStandardSignature".data(using: .utf8))) } - // MARK: ---- fails to sign if there is no open group + // MARK: ------ fails to sign if there is no open group it("fails to sign if there is no open group") { mockStorage.write { db in _ = try OpenGroup.deleteAll(db) @@ -726,7 +708,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest).to(beNil()) } - // MARK: ---- fails to sign if there is no user key pair + // MARK: ------ fails to sign if there is no user key pair it("fails to sign if there is no user key pair") { mockStorage.write { db in _ = try Identity.filter(id: .x25519PublicKey).deleteAll(db) @@ -757,7 +739,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest).to(beNil()) } - // MARK: ---- fails to sign if no signature is generated + // MARK: ------ fails to sign if no signature is generated it("fails to sign if no signature is generated") { mockCrypto.reset() // The 'keyPair' value doesn't equate so have to explicitly reset mockCrypto @@ -789,7 +771,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: -- when blinded + // MARK: ---- when blinded context("when blinded") { beforeEach { mockStorage.write { db in @@ -799,7 +781,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: ---- signs the message correctly + // MARK: ------ signs the message correctly it("signs the message correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedSend( @@ -820,7 +802,7 @@ class OpenGroupAPISpec: QuickSpec { expect(requestBody?.signature).to(equal("TestSogsSignature".data(using: .utf8))) } - // MARK: ---- fails to sign if there is no open group + // MARK: ------ fails to sign if there is no open group it("fails to sign if there is no open group") { mockStorage.write { db in _ = try OpenGroup.deleteAll(db) @@ -850,7 +832,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest).to(beNil()) } - // MARK: ---- fails to sign if there is no ed key pair key + // MARK: ------ fails to sign if there is no ed key pair key it("fails to sign if there is no ed key pair key") { mockStorage.write { db in _ = try Identity.filter(id: .ed25519PublicKey).deleteAll(db) @@ -881,7 +863,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest).to(beNil()) } - // MARK: ---- fails to sign if no signature is generated + // MARK: ------ fails to sign if no signature is generated it("fails to sign if no signature is generated") { mockCrypto .when { @@ -922,9 +904,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing an individual message request + // MARK: -- when preparing an individual message request context("when preparing an individual message request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedMessage( @@ -941,7 +923,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing an update message request + // MARK: -- when preparing an update message request context("when preparing an update message request") { beforeEach { mockStorage.write { db in @@ -954,7 +936,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedMessageUpdate( @@ -972,7 +954,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.request.httpMethod).to(equal("PUT")) } - // MARK: -- when unblinded + // MARK: ---- when unblinded context("when unblinded") { beforeEach { mockStorage.write { db in @@ -981,7 +963,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: ---- signs the message correctly + // MARK: ------ signs the message correctly it("signs the message correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedMessageUpdate( @@ -1001,7 +983,7 @@ class OpenGroupAPISpec: QuickSpec { expect(requestBody?.signature).to(equal("TestStandardSignature".data(using: .utf8))) } - // MARK: ---- fails to sign if there is no open group + // MARK: ------ fails to sign if there is no open group it("fails to sign if there is no open group") { mockStorage.write { db in _ = try OpenGroup.deleteAll(db) @@ -1030,7 +1012,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest).to(beNil()) } - // MARK: ---- fails to sign if there is no user key pair + // MARK: ------ fails to sign if there is no user key pair it("fails to sign if there is no user key pair") { mockStorage.write { db in _ = try Identity.filter(id: .x25519PublicKey).deleteAll(db) @@ -1060,7 +1042,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest).to(beNil()) } - // MARK: ---- fails to sign if no signature is generated + // MARK: ------ fails to sign if no signature is generated it("fails to sign if no signature is generated") { mockCrypto.reset() // The 'keyPair' value doesn't equate so have to explicitly reset mockCrypto @@ -1091,7 +1073,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: -- when blinded + // MARK: ---- when blinded context("when blinded") { beforeEach { mockStorage.write { db in @@ -1101,7 +1083,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: ---- signs the message correctly + // MARK: ------ signs the message correctly it("signs the message correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedMessageUpdate( @@ -1121,7 +1103,7 @@ class OpenGroupAPISpec: QuickSpec { expect(requestBody?.signature).to(equal("TestSogsSignature".data(using: .utf8))) } - // MARK: ---- fails to sign if there is no open group + // MARK: ------ fails to sign if there is no open group it("fails to sign if there is no open group") { mockStorage.write { db in _ = try OpenGroup.deleteAll(db) @@ -1150,7 +1132,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest).to(beNil()) } - // MARK: ---- fails to sign if there is no ed key pair key + // MARK: ------ fails to sign if there is no ed key pair key it("fails to sign if there is no ed key pair key") { mockStorage.write { db in _ = try Identity.filter(id: .ed25519PublicKey).deleteAll(db) @@ -1180,7 +1162,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest).to(beNil()) } - // MARK: ---- fails to sign if no signature is generated + // MARK: ------ fails to sign if no signature is generated it("fails to sign if no signature is generated") { mockCrypto .when { @@ -1220,9 +1202,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a delete message request + // MARK: -- when preparing a delete message request context("when preparing a delete message request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedMessageDelete( @@ -1239,9 +1221,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a delete all messages request + // MARK: -- when preparing a delete all messages request context("when preparing a delete all messages request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedMessagesDeleteAll( @@ -1258,9 +1240,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a pin message request + // MARK: -- when preparing a pin message request context("when preparing a pin message request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedPinMessage( @@ -1277,9 +1259,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing an unpin message request + // MARK: -- when preparing an unpin message request context("when preparing an unpin message request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUnpinMessage( @@ -1296,9 +1278,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing an unpin all request + // MARK: -- when preparing an unpin all request context("when preparing an unpin all request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUnpinAll( @@ -1314,9 +1296,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing an upload file request + // MARK: -- when preparing an upload file request context("when preparing an upload file request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUploadFile( @@ -1332,7 +1314,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.request.httpMethod).to(equal("POST")) } - // MARK: -- doesn't add a fileName to the content-disposition header when not provided + // MARK: ---- doesn't add a fileName to the content-disposition header when not provided it("doesn't add a fileName to the content-disposition header when not provided") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUploadFile( @@ -1348,7 +1330,7 @@ class OpenGroupAPISpec: QuickSpec { .toNot(contain("filename")) } - // MARK: -- adds the fileName to the content-disposition header when provided + // MARK: ---- adds the fileName to the content-disposition header when provided it("adds the fileName to the content-disposition header when provided") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUploadFile( @@ -1366,9 +1348,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a download file request + // MARK: -- when preparing a download file request context("when preparing a download file request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedDownloadFile( @@ -1385,9 +1367,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a send direct message request + // MARK: -- when preparing a send direct message request context("when preparing a send direct message request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedSend( @@ -1404,9 +1386,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a ban user request + // MARK: -- when preparing a ban user request context("when preparing a ban user request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUserBan( @@ -1423,7 +1405,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.request.httpMethod).to(equal("POST")) } - // MARK: -- does a global ban if no room tokens are provided + // MARK: ---- does a global ban if no room tokens are provided it("does a global ban if no room tokens are provided") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUserBan( @@ -1442,7 +1424,7 @@ class OpenGroupAPISpec: QuickSpec { expect(requestBody?.rooms).to(beNil()) } - // MARK: -- does room specific bans if room tokens are provided + // MARK: ---- does room specific bans if room tokens are provided it("does room specific bans if room tokens are provided") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUserBan( @@ -1462,9 +1444,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing an unban user request + // MARK: -- when preparing an unban user request context("when preparing an unban user request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUserUnban( @@ -1480,7 +1462,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.request.httpMethod).to(equal("POST")) } - // MARK: -- does a global unban if no room tokens are provided + // MARK: ---- does a global unban if no room tokens are provided it("does a global unban if no room tokens are provided") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUserUnban( @@ -1498,7 +1480,7 @@ class OpenGroupAPISpec: QuickSpec { expect(requestBody?.rooms).to(beNil()) } - // MARK: -- does room specific unbans if room tokens are provided + // MARK: ---- does room specific unbans if room tokens are provided it("does room specific unbans if room tokens are provided") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUserUnban( @@ -1517,9 +1499,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a user permissions request + // MARK: -- when preparing a user permissions request context("when preparing a user permissions request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUserModeratorUpdate( @@ -1538,7 +1520,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.request.httpMethod).to(equal("POST")) } - // MARK: -- does a global update if no room tokens are provided + // MARK: ---- does a global update if no room tokens are provided it("does a global update if no room tokens are provided") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUserModeratorUpdate( @@ -1559,7 +1541,7 @@ class OpenGroupAPISpec: QuickSpec { expect(requestBody?.rooms).to(beNil()) } - // MARK: -- does room specific updates if room tokens are provided + // MARK: ---- does room specific updates if room tokens are provided it("does room specific updates if room tokens are provided") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUserModeratorUpdate( @@ -1580,7 +1562,7 @@ class OpenGroupAPISpec: QuickSpec { expect(requestBody?.rooms).to(equal(["testRoom"])) } - // MARK: -- fails if neither moderator or admin are set + // MARK: ---- fails if neither moderator or admin are set it("fails if neither moderator or admin are set") { var preparationError: Error? let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in @@ -1607,9 +1589,9 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: - when preparing a ban and delete all request + // MARK: -- when preparing a ban and delete all request context("when preparing a ban and delete all request") { - // MARK: -- generates the request correctly + // MARK: ---- generates the request correctly it("generates the request correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in try OpenGroupAPI.preparedUserBanAndDeleteAllMessages( @@ -1629,7 +1611,7 @@ class OpenGroupAPISpec: QuickSpec { .to(equal(.roomDeleteMessages("testRoom", sessionId: "testUserId"))) } -// // MARK: -- bans the user from the specified room rather than globally +// // MARK: ---- bans the user from the specified room rather than globally // it("bans the user from the specified room rather than globally") { // let preparedRequest: OpenGroupAPI.PreparedSendData? = mockStorage.read { db in // try OpenGroupAPI.preparedUserBanAndDeleteAllMessages( @@ -1648,9 +1630,9 @@ class OpenGroupAPISpec: QuickSpec { // } } - // MARK: - when signing + // MARK: -- when signing context("when signing") { - // MARK: -- fails when there is no serverPublicKey + // MARK: ---- fails when there is no serverPublicKey it("fails when there is no serverPublicKey") { mockStorage.write { db in _ = try OpenGroup.deleteAll(db) @@ -1675,7 +1657,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest).to(beNil()) } - // MARK: -- fails when there is no userEdKeyPair + // MARK: ---- fails when there is no userEdKeyPair it("fails when there is no userEdKeyPair") { mockStorage.write { db in _ = try Identity.filter(id: .ed25519PublicKey).deleteAll(db) @@ -1701,7 +1683,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest).to(beNil()) } - // MARK: -- fails when the serverPublicKey is not a hex string + // MARK: ---- fails when the serverPublicKey is not a hex string it("fails when the serverPublicKey is not a hex string") { mockStorage.write { db in _ = try OpenGroup.updateAll(db, OpenGroup.Columns.publicKey.set(to: "TestString!!!")) @@ -1726,7 +1708,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest).to(beNil()) } - // MARK: -- when unblinded + // MARK: ---- when unblinded context("when unblinded") { beforeEach { mockStorage.write { db in @@ -1735,7 +1717,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: ---- signs correctly + // MARK: ------ signs correctly it("signs correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.Room]>? = mockStorage.read { db in try OpenGroupAPI.preparedRooms( @@ -1759,7 +1741,7 @@ class OpenGroupAPISpec: QuickSpec { .to(equal("TestSignature".bytes.toBase64())) } - // MARK: ---- fails when the signature is not generated + // MARK: ------ fails when the signature is not generated it("fails when the signature is not generated") { mockCrypto .when { try $0.perform(.signature(message: anyArray(), secretKey: anyArray())) } @@ -1785,7 +1767,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: -- when blinded + // MARK: ---- when blinded context("when blinded") { beforeEach { mockStorage.write { db in @@ -1795,7 +1777,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: ---- signs correctly + // MARK: ------ signs correctly it("signs correctly") { let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.Room]>? = mockStorage.read { db in try OpenGroupAPI.preparedRooms( @@ -1819,7 +1801,7 @@ class OpenGroupAPISpec: QuickSpec { .to(equal("TestSogsSignature".bytes.toBase64())) } - // MARK: ---- fails when the blindedKeyPair is not generated + // MARK: ------ fails when the blindedKeyPair is not generated it("fails when the blindedKeyPair is not generated") { mockCrypto .when { [dependencies = dependencies!] in @@ -1852,7 +1834,7 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest).to(beNil()) } - // MARK: ---- fails when the sogsSignature is not generated + // MARK: ------ fails when the sogsSignature is not generated it("fails when the sogsSignature is not generated") { mockCrypto .when { [dependencies = dependencies!] in @@ -1887,7 +1869,7 @@ class OpenGroupAPISpec: QuickSpec { } } - // MARK: -- when sending + // MARK: ---- when sending context("when sending") { beforeEach { mockNetwork @@ -1895,7 +1877,7 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(MockNetwork.response(type: [OpenGroupAPI.Room].self)) } - // MARK: -- triggers sending correctly + // MARK: ---- triggers sending correctly it("triggers sending correctly") { var response: (info: ResponseInfoType, data: [OpenGroupAPI.Room])? @@ -1916,7 +1898,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error).to(beNil()) } - // MARK: -- fails when not given prepared data + // MARK: ---- fails when not given prepared data it("fails when not given prepared data") { var response: (info: ResponseInfoType, data: [OpenGroupAPI.Room])? diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 46ada277e..9219e202d 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -13,158 +13,119 @@ import Nimble @testable import SessionMessagingKit class OpenGroupManagerSpec: QuickSpec { - // MARK: - Spec - - override func spec() { - var mockStorage: Storage! - var mockNetwork: MockNetwork! - var mockCrypto: MockCrypto! - var mockUserDefaults: MockUserDefaults! - var mockCaches: MockCaches! - var mockGeneralCache: MockGeneralCache! - var mockOGMCache: MockOGMCache! - var dependencies: Dependencies! - var disposables: [AnyCancellable] = [] + override class func spec() { + // MARK: Configuration - var testInteraction1: Interaction! - var testGroupThread: SessionThread! - var testOpenGroup: OpenGroup! - var testPollInfo: OpenGroupAPI.RoomPollInfo! - var testMessage: OpenGroupAPI.Message! - var testDirectMessage: OpenGroupAPI.DirectMessage! + @TestState var testInteraction1: Interaction! = Interaction( + id: 234, + serverHash: "TestServerHash", + messageUuid: nil, + threadId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + authorId: "TestAuthorId", + variant: .standardOutgoing, + body: "Test", + timestampMs: 123, + receivedAtTimestampMs: 124, + wasRead: false, + hasMention: false, + expiresInSeconds: nil, + expiresStartedAtMs: nil, + linkPreviewUrl: nil, + openGroupServerMessageId: nil, + openGroupWhisperMods: false, + openGroupWhisperTo: nil + ) + @TestState var testGroupThread: SessionThread! = SessionThread( + id: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + variant: .community + ) + @TestState var testOpenGroup: OpenGroup! = OpenGroup( + server: "testServer", + roomToken: "testRoom", + publicKey: TestConstants.publicKey, + isActive: true, + name: "Test", + roomDescription: nil, + imageId: nil, + imageData: nil, + userCount: 0, + infoUpdates: 10, + sequenceNumber: 5 + ) + @TestState var testPollInfo: OpenGroupAPI.RoomPollInfo! = OpenGroupAPI.RoomPollInfo.mockValue.with( + token: "testRoom", + activeUsers: 10, + details: .mockValue + ) + @TestState var testMessage: OpenGroupAPI.Message! = OpenGroupAPI.Message( + id: 127, + sender: "05\(TestConstants.publicKey)", + posted: 123, + edited: nil, + deleted: nil, + seqNo: 124, + whisper: false, + whisperMods: false, + whisperTo: nil, + base64EncodedData: [ + "Cg0KC1Rlc3RNZXNzYWdlg", + "AAAAAAAAAAAAAAAAAAAAA", + "AAAAAAAAAAAAAAAAAAAAA", + "AAAAAAAAAAAAAAAAAAAAA", + "AAAAAAAAAAAAAAAAAAAAA", + "AAAAAAAAAAAAAAAAAAAAA", + "AAAAAAAAAAAAAAAAAAAAA", + "AAAAAAAAAAAAAAAAAAAAA", + "AAAAAAAAAAAAAAAAAAAAA", + "AAAAAAAAAAAAAAAAAAAAA", + "AA" + ].joined(), + base64EncodedSignature: nil, + reactions: nil + ) + @TestState var testDirectMessage: OpenGroupAPI.DirectMessage! = OpenGroupAPI.DirectMessage( + id: 128, + sender: "15\(TestConstants.publicKey)", + recipient: "15\(TestConstants.publicKey)", + posted: 1234567890, + expires: 1234567990, + base64EncodedMessage: Data( + Bytes(arrayLiteral: 0) + + "TestMessage".bytes + + Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes + ).base64EncodedString() + ) - var cache: OpenGroupManager.Cache! - var openGroupManager: OpenGroupManager! - - describe("an OpenGroupManager") { - // MARK: - Configuration - - beforeEach { - mockStorage = SynchronousStorage( - customWriter: try! DatabaseQueue(), - customMigrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ] - ) - mockNetwork = MockNetwork() - mockCrypto = MockCrypto() - mockUserDefaults = MockUserDefaults() - mockCaches = MockCaches() - mockGeneralCache = MockGeneralCache() - mockOGMCache = MockOGMCache() - dependencies = Dependencies( - storage: mockStorage, - network: mockNetwork, - crypto: mockCrypto, - standardUserDefaults: mockUserDefaults, - caches: mockCaches, - dateNow: Date(timeIntervalSince1970: 1234567890), - forceSynchronous: true - ) - mockCaches[.general] = mockGeneralCache - mockCaches[.openGroupManager] = mockOGMCache - testInteraction1 = Interaction( - id: 234, - serverHash: "TestServerHash", - messageUuid: nil, - threadId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), - authorId: "TestAuthorId", - variant: .standardOutgoing, - body: "Test", - timestampMs: 123, - receivedAtTimestampMs: 124, - wasRead: false, - hasMention: false, - expiresInSeconds: nil, - expiresStartedAtMs: nil, - linkPreviewUrl: nil, - openGroupServerMessageId: nil, - openGroupWhisperMods: false, - openGroupWhisperTo: nil - ) + @TestState var mockStorage: Storage! = SynchronousStorage( + customWriter: try! DatabaseQueue(), + customMigrationTargets: [ + SNUtilitiesKit.self, + SNMessagingKit.self + ], + initialData: { db in + try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data.data(fromHex: TestConstants.privateKey)!).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: TestConstants.edPublicKey)!).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data.data(fromHex: TestConstants.edSecretKey)!).insert(db) - testGroupThread = SessionThread( - id: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), - variant: .community - ) - testOpenGroup = OpenGroup( - server: "testServer", - roomToken: "testRoom", - publicKey: TestConstants.publicKey, - isActive: true, - name: "Test", - roomDescription: nil, - imageId: nil, - imageData: nil, - userCount: 0, - infoUpdates: 10, - sequenceNumber: 5 - ) - testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with( - token: "testRoom", - activeUsers: 10, - details: .mockValue - ) - testMessage = OpenGroupAPI.Message( - id: 127, - sender: "05\(TestConstants.publicKey)", - posted: 123, - edited: nil, - deleted: nil, - seqNo: 124, - whisper: false, - whisperMods: false, - whisperTo: nil, - base64EncodedData: [ - "Cg0KC1Rlc3RNZXNzYWdlg", - "AAAAAAAAAAAAAAAAAAAAA", - "AAAAAAAAAAAAAAAAAAAAA", - "AAAAAAAAAAAAAAAAAAAAA", - "AAAAAAAAAAAAAAAAAAAAA", - "AAAAAAAAAAAAAAAAAAAAA", - "AAAAAAAAAAAAAAAAAAAAA", - "AAAAAAAAAAAAAAAAAAAAA", - "AAAAAAAAAAAAAAAAAAAAA", - "AAAAAAAAAAAAAAAAAAAAA", - "AA" - ].joined(), - base64EncodedSignature: nil, - reactions: nil - ) - testDirectMessage = OpenGroupAPI.DirectMessage( - id: 128, - sender: "15\(TestConstants.publicKey)", - recipient: "15\(TestConstants.publicKey)", - posted: 1234567890, - expires: 1234567990, - base64EncodedMessage: Data( - Bytes(arrayLiteral: 0) + - "TestMessage".bytes + - Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes - ).base64EncodedString() - ) - - mockStorage.write { db in - try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data.data(fromHex: TestConstants.privateKey)!).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: TestConstants.edPublicKey)!).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data.data(fromHex: TestConstants.edSecretKey)!).insert(db) - - try testGroupThread.insert(db) - try testOpenGroup.insert(db) - try Capability(openGroupServer: testOpenGroup.server, variant: .sogs, isMissing: false).insert(db) - } - mockCrypto + try testGroupThread.insert(db) + try testOpenGroup.insert(db) + try Capability(openGroupServer: testOpenGroup.server, variant: .sogs, isMissing: false).insert(db) + } + ) + @TestState var mockNetwork: MockNetwork! = MockNetwork() + @TestState var mockCrypto: MockCrypto! = MockCrypto( + initialSetup: { crypto in + crypto .when { try $0.perform(.hash(message: anyArray(), outputLength: any())) } .thenReturn([]) - mockCrypto - .when { [dependencies = dependencies!] crypto in + crypto + .when { crypto in crypto.generate( .blindedKeyPair( serverPublicKey: any(), edKeyPair: any(), - using: dependencies + using: any() ) ) } @@ -174,7 +135,7 @@ class OpenGroupManagerSpec: QuickSpec { secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ) ) - mockCrypto + crypto .when { try $0.perform( .sogsSignature( @@ -186,7 +147,7 @@ class OpenGroupManagerSpec: QuickSpec { ) } .thenReturn("TestSogsSignature".bytes) - mockCrypto + crypto .when { try $0.perform( .signature( @@ -196,59 +157,68 @@ class OpenGroupManagerSpec: QuickSpec { ) } .thenReturn("TestSignature".bytes) - mockCrypto.when { $0.size(.nonce16) }.thenReturn(16) - mockCrypto + crypto.when { $0.size(.nonce16) }.thenReturn(16) + crypto .when { try $0.perform(.generateNonce16()) } .thenReturn(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!.bytes) - mockCrypto.when { $0.size(.nonce24) }.thenReturn(24) - mockCrypto + crypto.when { $0.size(.nonce24) }.thenReturn(24) + crypto .when { try $0.perform(.generateNonce24()) } .thenReturn(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes) - mockCrypto.when { $0.size(.publicKey) }.thenReturn(32) - mockOGMCache.when { $0.pendingChanges }.thenReturn([]) - mockOGMCache.when { $0.pollers = any() }.thenReturn(()) - mockOGMCache.when { $0.isPolling = any() }.thenReturn(()) - mockOGMCache + crypto.when { $0.size(.publicKey) }.thenReturn(32) + } + ) + @TestState var mockUserDefaults: MockUserDefaults! = MockUserDefaults() + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache( + initialSetup: { cache in + cache.when { $0.encodedPublicKey }.thenReturn("05\(TestConstants.publicKey)") + } + ) + @TestState var mockOGMCache: MockOGMCache! = MockOGMCache( + initialSetup: { cache in + cache.when { $0.pendingChanges }.thenReturn([]) + cache.when { $0.pollers = any() }.thenReturn(()) + cache.when { $0.isPolling = any() }.thenReturn(()) + cache .when { $0.defaultRoomsPublisher = any(type: [OpenGroupManager.DefaultRoomInfo].self) } .thenReturn(()) - mockOGMCache + cache .when { $0.groupImagePublishers = any(typeA: String.self, typeB: AnyPublisher.self) } .thenReturn(()) - mockOGMCache + cache .when { $0.pendingChanges = any(type: OpenGroupAPI.PendingChange.self) } .thenReturn(()) - mockGeneralCache.when { $0.encodedPublicKey }.thenReturn("05\(TestConstants.publicKey)") - - cache = OpenGroupManager.Cache() - openGroupManager = OpenGroupManager() } - + ) + @TestState var mockCaches: MockCaches! = MockCaches() + .setting(cache: .general, to: mockGeneralCache) + .setting(cache: .openGroupManager, to: mockOGMCache) + @TestState var dependencies: Dependencies! = Dependencies( + storage: mockStorage, + network: mockNetwork, + crypto: mockCrypto, + standardUserDefaults: mockUserDefaults, + caches: mockCaches, + dateNow: Date(timeIntervalSince1970: 1234567890), + forceSynchronous: true + ) + @TestState var disposables: [AnyCancellable]! = [] + + @TestState var cache: OpenGroupManager.Cache! = OpenGroupManager.Cache() + @TestState var openGroupManager: OpenGroupManager! = OpenGroupManager() + + // MARK: - an OpenGroupManager + describe("an OpenGroupManager") { + afterEach { - disposables.forEach { $0.cancel() } - - OpenGroupManager.shared.stopPolling() // Need to stop any pollers which get created during tests - openGroupManager.stopPolling() // Assuming it's different from the above - - mockStorage = nil - mockCrypto = nil - mockUserDefaults = nil - mockCaches = nil - mockGeneralCache = nil - mockOGMCache = nil - dependencies = nil - disposables = [] - - testInteraction1 = nil - testGroupThread = nil - testOpenGroup = nil - - cache = nil - openGroupManager = nil + // Just in case the shared instance had pollers created we should stop them + OpenGroupManager.shared.stopPolling() + openGroupManager.stopPolling() } - // MARK: - cache data + // MARK: -- cache data context("cache data") { - // MARK: -- defaults the time since last open to greatestFiniteMagnitude + // MARK: ---- defaults the time since last open to greatestFiniteMagnitude it("defaults the time since last open to greatestFiniteMagnitude") { mockUserDefaults .when { (defaults: inout any UserDefaultsType) -> Any? in @@ -260,7 +230,7 @@ class OpenGroupManagerSpec: QuickSpec { .to(beCloseTo(.greatestFiniteMagnitude)) } - // MARK: -- returns the time since the last open + // MARK: ---- returns the time since the last open it("returns the time since the last open") { mockUserDefaults .when { (defaults: inout any UserDefaultsType) -> Any? in @@ -273,7 +243,7 @@ class OpenGroupManagerSpec: QuickSpec { .to(beCloseTo(10)) } - // MARK: -- caches the time since the last open + // MARK: ---- caches the time since the last open it("caches the time since the last open") { mockUserDefaults .when { (defaults: inout any UserDefaultsType) -> Any? in @@ -297,7 +267,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: - when starting polling + // MARK: -- when starting polling context("when starting polling") { beforeEach { mockStorage.write { db in @@ -332,7 +302,7 @@ class OpenGroupManagerSpec: QuickSpec { .thenReturn(Date(timeIntervalSince1970: 1234567890)) } - // MARK: -- creates pollers for all of the open groups + // MARK: ---- creates pollers for all of the open groups it("creates pollers for all of the open groups") { openGroupManager.startPolling(using: dependencies) @@ -345,7 +315,7 @@ class OpenGroupManagerSpec: QuickSpec { }) } - // MARK: -- updates the isPolling flag + // MARK: ---- updates the isPolling flag it("updates the isPolling flag") { openGroupManager.startPolling(using: dependencies) @@ -353,7 +323,7 @@ class OpenGroupManagerSpec: QuickSpec { .to(call(matchingParameters: true) { $0.isPolling = true }) } - // MARK: -- does nothing if already polling + // MARK: ---- does nothing if already polling it("does nothing if already polling") { mockOGMCache.when { $0.isPolling }.thenReturn(true) @@ -363,7 +333,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: - when stopping polling + // MARK: -- when stopping polling context("when stopping polling") { beforeEach { mockStorage.write { db in @@ -385,14 +355,14 @@ class OpenGroupManagerSpec: QuickSpec { mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) } - // MARK: - removes all pollers + // MARK: ---- removes all pollers it("removes all pollers") { openGroupManager.stopPolling(using: dependencies) expect(mockOGMCache).to(call(matchingParameters: true) { $0.pollers = [:] }) } - // MARK: - updates the isPolling flag + // MARK: ---- updates the isPolling flag it("updates the isPolling flag") { openGroupManager.stopPolling(using: dependencies) @@ -400,87 +370,87 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: - when checking if an open group is run by session + // MARK: -- when checking if an open group is run by session context("when checking if an open group is run by session") { - // MARK: -- returns false when it does not match one of Sessions servers with no scheme + // MARK: ---- returns false when it does not match one of Sessions servers with no scheme it("returns false when it does not match one of Sessions servers with no scheme") { expect(OpenGroupManager.isSessionRunOpenGroup(server: "test.test")) .to(beFalse()) } - // MARK: -- returns false when it does not match one of Sessions servers in http + // MARK: ---- returns false when it does not match one of Sessions servers in http it("returns false when it does not match one of Sessions servers in http") { expect(OpenGroupManager.isSessionRunOpenGroup(server: "http://test.test")) .to(beFalse()) } - // MARK: -- returns false when it does not match one of Sessions servers in https + // MARK: ---- returns false when it does not match one of Sessions servers in https it("returns false when it does not match one of Sessions servers in https") { expect(OpenGroupManager.isSessionRunOpenGroup(server: "https://test.test")) .to(beFalse()) } - // MARK: -- returns true when it matches Sessions SOGS IP + // MARK: ---- returns true when it matches Sessions SOGS IP it("returns true when it matches Sessions SOGS IP") { expect(OpenGroupManager.isSessionRunOpenGroup(server: "116.203.70.33")) .to(beTrue()) } - // MARK: -- returns true when it matches Sessions SOGS IP with http + // MARK: ---- returns true when it matches Sessions SOGS IP with http it("returns true when it matches Sessions SOGS IP with http") { expect(OpenGroupManager.isSessionRunOpenGroup(server: "http://116.203.70.33")) .to(beTrue()) } - // MARK: -- returns true when it matches Sessions SOGS IP with https + // MARK: ---- returns true when it matches Sessions SOGS IP with https it("returns true when it matches Sessions SOGS IP with https") { expect(OpenGroupManager.isSessionRunOpenGroup(server: "https://116.203.70.33")) .to(beTrue()) } - // MARK: -- returns true when it matches Sessions SOGS IP with a port + // MARK: ---- returns true when it matches Sessions SOGS IP with a port it("returns true when it matches Sessions SOGS IP with a port") { expect(OpenGroupManager.isSessionRunOpenGroup(server: "116.203.70.33:80")) .to(beTrue()) } - // MARK: -- returns true when it matches Sessions SOGS domain + // MARK: ---- returns true when it matches Sessions SOGS domain it("returns true when it matches Sessions SOGS domain") { expect(OpenGroupManager.isSessionRunOpenGroup(server: "open.getsession.org")) .to(beTrue()) } - // MARK: -- returns true when it matches Sessions SOGS domain with http + // MARK: ---- returns true when it matches Sessions SOGS domain with http it("returns true when it matches Sessions SOGS domain with http") { expect(OpenGroupManager.isSessionRunOpenGroup(server: "http://open.getsession.org")) .to(beTrue()) } - // MARK: -- returns true when it matches Sessions SOGS domain with https + // MARK: ---- returns true when it matches Sessions SOGS domain with https it("returns true when it matches Sessions SOGS domain with https") { expect(OpenGroupManager.isSessionRunOpenGroup(server: "https://open.getsession.org")) .to(beTrue()) } - // MARK: -- returns true when it matches Sessions SOGS domain with a port + // MARK: ---- returns true when it matches Sessions SOGS domain with a port it("returns true when it matches Sessions SOGS domain with a port") { expect(OpenGroupManager.isSessionRunOpenGroup(server: "open.getsession.org:80")) .to(beTrue()) } } - // MARK: - when checking it has an existing open group + // MARK: -- when checking it has an existing open group context("when checking it has an existing open group") { - // MARK: -- when there is a thread for the room and the cache has a poller + // MARK: ---- when there is a thread for the room and the cache has a poller context("when there is a thread for the room and the cache has a poller") { - // MARK: ---- for the no-scheme variant + // MARK: ------ for the no-scheme variant context("for the no-scheme variant") { beforeEach { mockOGMCache.when { $0.pollers } .thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) } - // MARK: ------ returns true when no scheme is provided + // MARK: -------- returns true when no scheme is provided it("returns true when no scheme is provided") { expect( mockStorage.read { db -> Bool in @@ -496,7 +466,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: ------ returns true when a http scheme is provided + // MARK: -------- returns true when a http scheme is provided it("returns true when a http scheme is provided") { expect( mockStorage.read { db -> Bool in @@ -512,7 +482,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: ------ returns true when a https scheme is provided + // MARK: -------- returns true when a https scheme is provided it("returns true when a https scheme is provided") { expect( mockStorage.read { db -> Bool in @@ -529,14 +499,14 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: ---- for the http variant + // MARK: ------ for the http variant context("for the http variant") { beforeEach { mockOGMCache.when { $0.pollers } .thenReturn(["http://testserver": OpenGroupAPI.Poller(for: "http://testserver")]) } - // MARK: ------ returns true when no scheme is provided + // MARK: -------- returns true when no scheme is provided it("returns true when no scheme is provided") { expect( mockStorage.read { db -> Bool in @@ -552,7 +522,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: ------ returns true when a http scheme is provided + // MARK: -------- returns true when a http scheme is provided it("returns true when a http scheme is provided") { expect( mockStorage.read { db -> Bool in @@ -568,7 +538,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: ------ returns true when a https scheme is provided + // MARK: -------- returns true when a https scheme is provided it("returns true when a https scheme is provided") { expect( mockStorage.read { db -> Bool in @@ -585,14 +555,14 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: ---- for the https variant + // MARK: ------ for the https variant context("for the https variant") { beforeEach { mockOGMCache.when { $0.pollers } .thenReturn(["https://testserver": OpenGroupAPI.Poller(for: "https://testserver")]) } - // MARK: ------ returns true when no scheme is provided + // MARK: -------- returns true when no scheme is provided it("returns true when no scheme is provided") { expect( mockStorage.read { db -> Bool in @@ -608,7 +578,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: ------ returns true when a http scheme is provided + // MARK: -------- returns true when a http scheme is provided it("returns true when a http scheme is provided") { expect( mockStorage.read { db -> Bool in @@ -624,7 +594,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: ------ returns true when a https scheme is provided + // MARK: -------- returns true when a https scheme is provided it("returns true when a https scheme is provided") { expect( mockStorage.read { db -> Bool in @@ -642,9 +612,9 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- when given the legacy DNS host and there is a cached poller for the default server + // MARK: ---- when given the legacy DNS host and there is a cached poller for the default server context("when given the legacy DNS host and there is a cached poller for the default server") { - // MARK: ---- returns true + // MARK: ------ returns true it("returns true") { mockOGMCache.when { $0.pollers } .thenReturn(["http://116.203.70.33": OpenGroupAPI.Poller(for: "http://116.203.70.33")]) @@ -677,9 +647,9 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- when given the default server and there is a cached poller for the legacy DNS host + // MARK: ---- when given the default server and there is a cached poller for the legacy DNS host context("when given the default server and there is a cached poller for the legacy DNS host") { - // MARK: ---- returns true + // MARK: ------ returns true it("returns true") { mockOGMCache.when { $0.pollers }.thenReturn(["http://open.getsession.org": OpenGroupAPI.Poller(for: "http://open.getsession.org")]) mockStorage.write { db in @@ -711,7 +681,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- returns false when given an invalid server + // MARK: ---- returns false when given an invalid server it("returns false when given an invalid server") { mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) @@ -729,7 +699,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beFalse()) } - // MARK: -- returns false if there is not a poller for the server in the cache + // MARK: ---- returns false if there is not a poller for the server in the cache it("returns false if there is not a poller for the server in the cache") { mockOGMCache.when { $0.pollers }.thenReturn([:]) @@ -747,7 +717,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beFalse()) } - // MARK: -- returns false if there is a poller for the server in the cache but no thread for the room + // MARK: ---- returns false if there is a poller for the server in the cache but no thread for the room it("returns false if there is a poller for the server in the cache but no thread for the room") { mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) mockStorage.write { db in @@ -769,7 +739,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: - when adding + // MARK: -- when adding context("when adding") { beforeEach { mockStorage.write { db in @@ -788,7 +758,7 @@ class OpenGroupManagerSpec: QuickSpec { .thenReturn(Date(timeIntervalSince1970: 1234567890)) } - // MARK: -- stores the open group server + // MARK: ---- stores the open group server it("stores the open group server") { mockStorage .writePublisher { (db: Database) -> Bool in @@ -826,7 +796,7 @@ class OpenGroupManagerSpec: QuickSpec { .to(equal(OpenGroup.idFor(roomToken: "testRoom", server: "testServer"))) } - // MARK: -- adds a poller + // MARK: ---- adds a poller it("adds a poller") { mockStorage .writePublisher { (db: Database) -> Bool in @@ -858,7 +828,7 @@ class OpenGroupManagerSpec: QuickSpec { }) } - // MARK: -- an existing room + // MARK: ---- an existing room context("an existing room") { beforeEach { mockOGMCache.when { $0.pollers } @@ -868,7 +838,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: ---- does not reset the sequence number or update the public key + // MARK: ------ does not reset the sequence number or update the public key it("does not reset the sequence number or update the public key") { mockStorage .writePublisher { (db: Database) -> Bool in @@ -919,7 +889,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- with an invalid response + // MARK: ---- with an invalid response context("with an invalid response") { beforeEach { mockNetwork @@ -933,7 +903,7 @@ class OpenGroupManagerSpec: QuickSpec { .thenReturn(Date(timeIntervalSince1970: 1234567890)) } - // MARK: ---- fails with the error + // MARK: ------ fails with the error it("fails with the error") { var error: Error? @@ -967,7 +937,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: - when deleting + // MARK: -- when deleting context("when deleting") { beforeEach { mockStorage.write { db in @@ -988,7 +958,7 @@ class OpenGroupManagerSpec: QuickSpec { mockOGMCache.when { $0.pollers }.thenReturn([:]) } - // MARK: -- removes all interactions for the thread + // MARK: ---- removes all interactions for the thread it("removes all interactions for the thread") { mockStorage.write { db in openGroupManager @@ -1004,7 +974,7 @@ class OpenGroupManagerSpec: QuickSpec { .to(equal(0)) } - // MARK: -- removes the given thread + // MARK: ---- removes the given thread it("removes the given thread") { mockStorage.write { db in openGroupManager @@ -1020,9 +990,9 @@ class OpenGroupManagerSpec: QuickSpec { .to(equal(0)) } - // MARK: -- and there is only one open group for this server + // MARK: ---- and there is only one open group for this server context("and there is only one open group for this server") { - // MARK: ---- stops the poller + // MARK: ------ stops the poller it("stops the poller") { mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) @@ -1039,7 +1009,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockOGMCache).to(call(matchingParameters: true) { $0.pollers = [:] }) } - // MARK: ---- removes the open group + // MARK: ------ removes the open group it("removes the open group") { mockStorage.write { db in openGroupManager @@ -1056,7 +1026,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- and the are multiple open groups for this server + // MARK: ---- and the are multiple open groups for this server context("and the are multiple open groups for this server") { beforeEach { mockStorage.write { db in @@ -1080,7 +1050,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: ---- removes the open group + // MARK: ------ removes the open group it("removes the open group") { mockStorage.write { db in openGroupManager @@ -1097,7 +1067,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- and it is the default server + // MARK: ---- and it is the default server context("and it is the default server") { beforeEach { mockStorage.write { db in @@ -1135,7 +1105,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: ---- does not remove the open group + // MARK: ------ does not remove the open group it("does not remove the open group") { mockStorage.write { db in openGroupManager @@ -1151,7 +1121,7 @@ class OpenGroupManagerSpec: QuickSpec { .to(equal(2)) } - // MARK: ---- deactivates the open group + // MARK: ------ deactivates the open group it("deactivates the open group") { mockStorage.write { db in openGroupManager @@ -1176,7 +1146,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: - when handling capabilities + // MARK: -- when handling capabilities context("when handling capabilities") { beforeEach { mockStorage.write { db in @@ -1189,14 +1159,14 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- stores the capabilities + // MARK: ---- stores the capabilities it("stores the capabilities") { expect(mockStorage.read { db in try Capability.fetchCount(db) }) .to(equal(1)) } } - // MARK: - when handling room poll info + // MARK: -- when handling room poll info context("when handling room poll info") { beforeEach { mockStorage.write { db in @@ -1214,7 +1184,7 @@ class OpenGroupManagerSpec: QuickSpec { .thenReturn(nil) } - // MARK: -- saves the updated open group + // MARK: ---- saves the updated open group it("saves the updated open group") { mockStorage.write { db in try OpenGroupManager.handlePollInfo( @@ -1237,7 +1207,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(equal(10)) } - // MARK: -- calls the completion block + // MARK: ---- calls the completion block it("calls the completion block") { var didCallComplete: Bool = false @@ -1255,7 +1225,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(didCallComplete).to(beTrue()) } - // MARK: -- calls the room image completion block when waiting but there is no image + // MARK: ---- calls the room image completion block when waiting but there is no image it("calls the room image completion block when waiting but there is no image") { var didCallComplete: Bool = false @@ -1274,7 +1244,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(didCallComplete).to(beTrue()) } - // MARK: -- calls the room image completion block when waiting and there is an image + // MARK: ---- calls the room image completion block when waiting and there is an image it("calls the room image completion block when waiting and there is an image") { var didCallComplete: Bool = false @@ -1313,9 +1283,9 @@ class OpenGroupManagerSpec: QuickSpec { expect(didCallComplete).to(beTrue()) } - // MARK: -- and updating the moderator list + // MARK: ---- and updating the moderator list context("and updating the moderator list") { - // MARK: ---- successfully updates + // MARK: ------ successfully updates it("successfully updates") { testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with( token: "testRoom", @@ -1361,7 +1331,7 @@ class OpenGroupManagerSpec: QuickSpec { )) } - // MARK: ---- updates for hidden moderators + // MARK: ------ updates for hidden moderators it("updates for hidden moderators") { testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with( token: "testRoom", @@ -1407,7 +1377,7 @@ class OpenGroupManagerSpec: QuickSpec { )) } - // MARK: ---- does not insert mods if no moderators are provided + // MARK: ------ does not insert mods if no moderators are provided it("does not insert mods if no moderators are provided") { testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with( token: "testRoom", @@ -1430,9 +1400,9 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- and updating the admin list + // MARK: ---- and updating the admin list context("and updating the admin list") { - // MARK: ---- successfully updates + // MARK: ------ successfully updates it("successfully updates") { testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with( token: "testRoom", @@ -1478,7 +1448,7 @@ class OpenGroupManagerSpec: QuickSpec { )) } - // MARK: ---- updates for hidden admins + // MARK: ------ updates for hidden admins it("updates for hidden admins") { testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with( token: "testRoom", @@ -1524,7 +1494,7 @@ class OpenGroupManagerSpec: QuickSpec { )) } - // MARK: ---- does not insert an admin if no admins are provided + // MARK: ------ does not insert an admin if no admins are provided it("does not insert an admin if no admins are provided") { testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with( token: "testRoom", @@ -1548,9 +1518,9 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- when it cannot get the open group + // MARK: ---- when it cannot get the open group context("when it cannot get the open group") { - // MARK: ---- does not save the thread + // MARK: ------ does not save the thread it("does not save the thread") { mockStorage.write { db in try OpenGroup.deleteAll(db) @@ -1571,9 +1541,9 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- when not given a public key + // MARK: ---- when not given a public key context("when not given a public key") { - // MARK: ---- saves the open group with the existing public key + // MARK: ------ saves the open group with the existing public key it("saves the open group with the existing public key") { mockStorage.write { db in try OpenGroupManager.handlePollInfo( @@ -1597,9 +1567,9 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- when checking to start polling + // MARK: ---- when checking to start polling context("when checking to start polling") { - // MARK: ---- starts a new poller when not already polling + // MARK: ------ starts a new poller when not already polling it("starts a new poller when not already polling") { mockOGMCache.when { $0.pollers }.thenReturn([:]) @@ -1620,7 +1590,7 @@ class OpenGroupManagerSpec: QuickSpec { }) } - // MARK: ---- does not start a new poller when already polling + // MARK: ------ does not start a new poller when already polling it("does not start a new poller when already polling") { mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) @@ -1639,7 +1609,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- when trying to get the room image + // MARK: ---- when trying to get the room image context("when trying to get the room image") { beforeEach { let image: UIImage = UIImage(color: .red, size: CGSize(width: 1, height: 1)) @@ -1656,7 +1626,7 @@ class OpenGroupManagerSpec: QuickSpec { ]) } - // MARK: ---- uses the provided room image id if available + // MARK: ------ uses the provided room image id if available it("uses the provided room image id if available") { testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with( token: "testRoom", @@ -1698,7 +1668,7 @@ class OpenGroupManagerSpec: QuickSpec { ).toNot(beNil()) } - // MARK: ---- uses the existing room image id if none is provided + // MARK: ------ uses the existing room image id if none is provided it("uses the existing room image id if none is provided") { mockStorage.write { db in try OpenGroup.deleteAll(db) @@ -1751,7 +1721,7 @@ class OpenGroupManagerSpec: QuickSpec { ).toNot(beNil()) } - // MARK: ---- uses the new room image id if there is an existing one + // MARK: ------ uses the new room image id if there is an existing one it("uses the new room image id if there is an existing one") { mockStorage.write { db in try OpenGroup.deleteAll(db) @@ -1810,7 +1780,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockOGMCache).to(call(.exactly(times: 1)) { $0.groupImagePublishers }) } - // MARK: ---- does nothing if there is no room image + // MARK: ------ does nothing if there is no room image it("does nothing if there is no room image") { mockStorage.write { db in try OpenGroupManager.handlePollInfo( @@ -1834,7 +1804,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beNil()) } - // MARK: ---- does nothing if it fails to retrieve the room image + // MARK: ------ does nothing if it fails to retrieve the room image it("does nothing if it fails to retrieve the room image") { mockOGMCache.when { $0.groupImagePublishers } .thenReturn([ @@ -1873,7 +1843,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beNil()) } - // MARK: ---- saves the retrieved room image + // MARK: ------ saves the retrieved room image it("saves the retrieved room image") { testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with( token: "testRoom", @@ -1909,7 +1879,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: - when handling messages + // MARK: -- when handling messages context("when handling messages") { beforeEach { mockStorage.write { db in @@ -1919,7 +1889,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- updates the sequence number when there are messages + // MARK: ---- updates the sequence number when there are messages it("updates the sequence number when there are messages") { mockStorage.write { db in OpenGroupManager.handleMessages( @@ -1956,7 +1926,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(equal(124)) } - // MARK: -- does not update the sequence number if there are no messages + // MARK: ---- does not update the sequence number if there are no messages it("does not update the sequence number if there are no messages") { mockStorage.write { db in OpenGroupManager.handleMessages( @@ -1978,7 +1948,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(equal(5)) } - // MARK: -- ignores a message with no sender + // MARK: ---- ignores a message with no sender it("ignores a message with no sender") { mockStorage.write { db in try Interaction.deleteAll(db) @@ -2012,7 +1982,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(0)) } - // MARK: -- ignores a message with invalid data + // MARK: ---- ignores a message with invalid data it("ignores a message with invalid data") { mockStorage.write { db in try Interaction.deleteAll(db) @@ -2046,7 +2016,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(0)) } - // MARK: -- processes a message with valid data + // MARK: ---- processes a message with valid data it("processes a message with valid data") { mockStorage.write { db in OpenGroupManager.handleMessages( @@ -2061,7 +2031,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(1)) } - // MARK: -- processes valid messages when combined with invalid ones + // MARK: ---- processes valid messages when combined with invalid ones it("processes valid messages when combined with invalid ones") { mockStorage.write { db in OpenGroupManager.handleMessages( @@ -2092,9 +2062,9 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(1)) } - // MARK: -- with no data + // MARK: ---- with no data context("with no data") { - // MARK: ---- deletes the message if we have the message + // MARK: ------ deletes the message if we have the message it("deletes the message if we have the message") { mockStorage.write { db in try Interaction @@ -2132,7 +2102,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(0)) } - // MARK: ---- does nothing if we do not have the message + // MARK: ------ does nothing if we do not have the message it("does nothing if we do not have the message") { mockStorage.write { db in OpenGroupManager.handleMessages( @@ -2164,7 +2134,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: - when handling direct messages + // MARK: -- when handling direct messages context("when handling direct messages") { beforeEach { mockCrypto @@ -2206,7 +2176,7 @@ class OpenGroupManagerSpec: QuickSpec { .thenReturn(Data(hex: TestConstants.publicKey).bytes) } - // MARK: -- does nothing if there are no messages + // MARK: ---- does nothing if there are no messages it("does nothing if there are no messages") { mockStorage.write { db in OpenGroupManager.handleDirectMessages( @@ -2236,7 +2206,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(equal(0)) } - // MARK: -- does nothing if it cannot get the open group + // MARK: ---- does nothing if it cannot get the open group it("does nothing if it cannot get the open group") { mockStorage.write { db in try OpenGroup.deleteAll(db) @@ -2270,7 +2240,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beNil()) } - // MARK: -- ignores messages with non base64 encoded data + // MARK: ---- ignores messages with non base64 encoded data it("ignores messages with non base64 encoded data") { testDirectMessage = OpenGroupAPI.DirectMessage( id: testDirectMessage.id, @@ -2294,7 +2264,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(0)) } - // MARK: -- for the inbox + // MARK: ---- for the inbox context("for the inbox") { beforeEach { mockCrypto @@ -2314,7 +2284,7 @@ class OpenGroupManagerSpec: QuickSpec { .thenReturn(false) } - // MARK: ---- updates the inbox latest message id + // MARK: ------ updates the inbox latest message id it("updates the inbox latest message id") { mockStorage.write { db in OpenGroupManager.handleDirectMessages( @@ -2336,7 +2306,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(equal(128)) } - // MARK: ---- ignores a message with invalid data + // MARK: ------ ignores a message with invalid data it("ignores a message with invalid data") { testDirectMessage = OpenGroupAPI.DirectMessage( id: testDirectMessage.id, @@ -2360,7 +2330,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(0)) } - // MARK: ---- processes a message with valid data + // MARK: ------ processes a message with valid data it("processes a message with valid data") { mockStorage.write { db in OpenGroupManager.handleDirectMessages( @@ -2375,7 +2345,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockStorage.read { db -> Int in try Interaction.fetchCount(db) }).to(equal(1)) } - // MARK: ---- processes valid messages when combined with invalid ones + // MARK: ------ processes valid messages when combined with invalid ones it("processes valid messages when combined with invalid ones") { mockStorage.write { db in OpenGroupManager.handleDirectMessages( @@ -2401,7 +2371,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- for the outbox + // MARK: ---- for the outbox context("for the outbox") { beforeEach { mockCrypto @@ -2421,7 +2391,7 @@ class OpenGroupManagerSpec: QuickSpec { .thenReturn(false) } - // MARK: ---- updates the outbox latest message id + // MARK: ------ updates the outbox latest message id it("updates the outbox latest message id") { mockStorage.write { db in OpenGroupManager.handleDirectMessages( @@ -2443,7 +2413,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(equal(128)) } - // MARK: ---- retrieves an existing blinded id lookup + // MARK: ------ retrieves an existing blinded id lookup it("retrieves an existing blinded id lookup") { mockStorage.write { db in try BlindedIdLookup( @@ -2468,7 +2438,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockStorage.read { db -> Int in try SessionThread.fetchCount(db) }).to(equal(2)) } - // MARK: ---- falls back to using the blinded id if no lookup is found + // MARK: ------ falls back to using the blinded id if no lookup is found it("falls back to using the blinded id if no lookup is found") { mockStorage.write { db in OpenGroupManager.handleDirectMessages( @@ -2497,7 +2467,7 @@ class OpenGroupManagerSpec: QuickSpec { ).toNot(beNil()) } - // MARK: ---- ignores a message with invalid data + // MARK: ------ ignores a message with invalid data it("ignores a message with invalid data") { testDirectMessage = OpenGroupAPI.DirectMessage( id: testDirectMessage.id, @@ -2521,7 +2491,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockStorage.read { db -> Int in try SessionThread.fetchCount(db) }).to(equal(1)) } - // MARK: ---- processes a message with valid data + // MARK: ------ processes a message with valid data it("processes a message with valid data") { mockStorage.write { db in OpenGroupManager.handleDirectMessages( @@ -2536,7 +2506,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockStorage.read { db -> Int in try SessionThread.fetchCount(db) }).to(equal(2)) } - // MARK: ---- processes valid messages when combined with invalid ones + // MARK: ------ processes valid messages when combined with invalid ones it("processes valid messages when combined with invalid ones") { mockStorage.write { db in OpenGroupManager.handleDirectMessages( @@ -2563,7 +2533,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: - when determining if a user is a moderator or an admin + // MARK: -- when determining if a user is a moderator or an admin context("when determining if a user is a moderator or an admin") { beforeEach { mockStorage.write { db in @@ -2571,7 +2541,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- uses an empty set for moderators by default + // MARK: ---- uses an empty set for moderators by default it("uses an empty set for moderators by default") { expect( OpenGroupManager.isUserModeratorOrAdmin( @@ -2583,7 +2553,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beFalse()) } - // MARK: -- uses an empty set for admins by default + // MARK: ---- uses an empty set for admins by default it("uses an empty set for admins by default") { expect( OpenGroupManager.isUserModeratorOrAdmin( @@ -2595,7 +2565,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beFalse()) } - // MARK: -- returns true if the key is in the moderator set + // MARK: ---- returns true if the key is in the moderator set it("returns true if the key is in the moderator set") { mockStorage.write { db in try GroupMember( @@ -2616,7 +2586,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: -- returns true if the key is in the admin set + // MARK: ---- returns true if the key is in the admin set it("returns true if the key is in the admin set") { mockStorage.write { db in try GroupMember( @@ -2637,7 +2607,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: -- returns true if the moderator is hidden + // MARK: ---- returns true if the moderator is hidden it("returns true if the moderator is hidden") { mockStorage.write { db in try GroupMember( @@ -2658,7 +2628,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: -- returns true if the admin is hidden + // MARK: ---- returns true if the admin is hidden it("returns true if the admin is hidden") { mockStorage.write { db in try GroupMember( @@ -2679,7 +2649,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: -- returns false if the key is not a valid session id + // MARK: ---- returns false if the key is not a valid session id it("returns false if the key is not a valid session id") { expect( OpenGroupManager.isUserModeratorOrAdmin( @@ -2691,9 +2661,9 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beFalse()) } - // MARK: -- and the key is a standard session id + // MARK: ---- and the key is a standard session id context("and the key is a standard session id") { - // MARK: ---- returns false if the key is not the users session id + // MARK: ------ returns false if the key is not the users session id it("returns false if the key is not the users session id") { mockStorage.write { db in let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6") @@ -2712,7 +2682,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beFalse()) } - // MARK: ---- returns true if the key is the current users and the users unblinded id is a moderator or admin + // MARK: ------ returns true if the key is the current users and the users unblinded id is a moderator or admin it("returns true if the key is the current users and the users unblinded id is a moderator or admin") { mockStorage.write { db in let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6") @@ -2738,7 +2708,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: ---- returns true if the key is the current users and the users blinded id is a moderator or admin + // MARK: ------ returns true if the key is the current users and the users blinded id is a moderator or admin it("returns true if the key is the current users and the users blinded id is a moderator or admin") { let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6") mockCrypto @@ -2777,9 +2747,9 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- and the key is unblinded + // MARK: ---- and the key is unblinded context("and the key is unblinded") { - // MARK: ---- returns false if unable to retrieve the user ed25519 key + // MARK: ------ returns false if unable to retrieve the user ed25519 key it("returns false if unable to retrieve the user ed25519 key") { mockStorage.write { db in try Identity.filter(id: .ed25519PublicKey).deleteAll(db) @@ -2796,7 +2766,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beFalse()) } - // MARK: ---- returns false if the key is not the users unblinded id + // MARK: ------ returns false if the key is not the users unblinded id it("returns false if the key is not the users unblinded id") { mockStorage.write { db in let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6") @@ -2815,7 +2785,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beFalse()) } - // MARK: ---- returns true if the key is the current users and the users session id is a moderator or admin + // MARK: ------ returns true if the key is the current users and the users session id is a moderator or admin it("returns true if the key is the current users and the users session id is a moderator or admin") { let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6") mockGeneralCache.when { $0.encodedPublicKey }.thenReturn("05\(otherKey)") @@ -2843,7 +2813,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: ---- returns true if the key is the current users and the users blinded id is a moderator or admin + // MARK: ------ returns true if the key is the current users and the users blinded id is a moderator or admin it("returns true if the key is the current users and the users blinded id is a moderator or admin") { let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6") mockCrypto @@ -2887,9 +2857,9 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- and the key is blinded + // MARK: ---- and the key is blinded context("and the key is blinded") { - // MARK: ---- returns false if unable to retrieve the user ed25519 key + // MARK: ------ returns false if unable to retrieve the user ed25519 key it("returns false if unable to retrieve the user ed25519 key") { mockStorage.write { db in try Identity.filter(id: .ed25519PublicKey).deleteAll(db) @@ -2906,7 +2876,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beFalse()) } - // MARK: ---- returns false if unable generate a blinded key + // MARK: ------ returns false if unable generate a blinded key it("returns false if unable generate a blinded key") { mockCrypto .when { [dependencies = dependencies!] crypto in @@ -2930,7 +2900,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beFalse()) } - // MARK: ---- returns false if the key is not the users blinded id + // MARK: ------ returns false if the key is not the users blinded id it("returns false if the key is not the users blinded id") { let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6") mockCrypto @@ -2960,7 +2930,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beFalse()) } - // MARK: ---- returns true if the key is the current users and the users session id is a moderator or admin + // MARK: ------ returns true if the key is the current users and the users session id is a moderator or admin it("returns true if the key is the current users and the users session id is a moderator or admin") { let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6") mockGeneralCache.when { $0.encodedPublicKey }.thenReturn("05\(otherKey)") @@ -3004,7 +2974,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beTrue()) } - // MARK: ---- returns true if the key is the current users and the users unblinded id is a moderator or admin + // MARK: ------ returns true if the key is the current users and the users unblinded id is a moderator or admin it("returns true if the key is the current users and the users unblinded id is a moderator or admin") { mockCrypto .when { [dependencies = dependencies!] crypto in @@ -3050,7 +3020,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: - when getting the default rooms if needed + // MARK: -- when getting the default rooms if needed context("when getting the default rooms if needed") { beforeEach { mockNetwork @@ -3083,7 +3053,7 @@ class OpenGroupManagerSpec: QuickSpec { }.thenReturn(()) } - // MARK: -- caches the publisher if there is no cached publisher + // MARK: ---- caches the publisher if there is no cached publisher it("caches the publisher if there is no cached publisher") { let publisher = OpenGroupManager.getDefaultRoomsIfNeeded(using: dependencies) @@ -3093,7 +3063,7 @@ class OpenGroupManagerSpec: QuickSpec { }) } - // MARK: -- returns the cached publisher if there is one + // MARK: ---- returns the cached publisher if there is one it("returns the cached publisher if there is one") { let uniqueRoomInstance: OpenGroupAPI.Room = OpenGroupAPI.Room.mockValue.with( token: "UniqueId", @@ -3111,7 +3081,7 @@ class OpenGroupManagerSpec: QuickSpec { .to(equal(publisher.firstValue()?.map { $0.room })) } - // MARK: -- stores the open group information + // MARK: ---- stores the open group information it("stores the open group information") { OpenGroupManager.getDefaultRoomsIfNeeded(using: dependencies) @@ -3144,7 +3114,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beFalse()) } - // MARK: -- fetches rooms for the server + // MARK: ---- fetches rooms for the server it("fetches rooms for the server") { var response: [OpenGroupManager.DefaultRoomInfo]? @@ -3156,7 +3126,7 @@ class OpenGroupManagerSpec: QuickSpec { .to(equal([OpenGroupAPI.Room.mockValue])) } - // MARK: -- will retry fetching rooms 8 times before it fails + // MARK: ---- will retry fetching rooms 8 times before it fails it("will retry fetching rooms 8 times before it fails") { mockNetwork .when { $0.send(.onionRequest(any(), to: any(), with: any())) } @@ -3173,7 +3143,7 @@ class OpenGroupManagerSpec: QuickSpec { .to(call(.exactly(times: 9)) { $0.send(.onionRequest(any(), to: any(), with: any())) }) } - // MARK: -- removes the cache publisher if all retries fail + // MARK: ---- removes the cache publisher if all retries fail it("removes the cache publisher if all retries fail") { mockNetwork .when { $0.send(.onionRequest(any(), to: any(), with: any())) } @@ -3193,7 +3163,7 @@ class OpenGroupManagerSpec: QuickSpec { }) } - // MARK: -- fetches the image for any rooms with images + // MARK: ---- fetches the image for any rooms with images it("fetches the image for any rooms with images") { mockNetwork .when { @@ -3260,7 +3230,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: - when getting a room image + // MARK: -- when getting a room image context("when getting a room image") { beforeEach { mockNetwork @@ -3289,7 +3259,7 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: -- retrieves the image retrieval publisher from the cache if it exists + // MARK: ---- retrieves the image retrieval publisher from the cache if it exists it("retrieves the image retrieval publisher from the cache if it exists") { let publisher = Future { resolver in resolver(Result.success(Data([5, 4, 3, 2, 1]))) @@ -3315,7 +3285,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(result).to(equal(publisher.firstValue())) } - // MARK: -- does not save the fetched image to storage + // MARK: ---- does not save the fetched image to storage it("does not save the fetched image to storage") { OpenGroupManager .roomImage( @@ -3338,7 +3308,7 @@ class OpenGroupManagerSpec: QuickSpec { ).to(beNil()) } - // MARK: -- does not update the image update timestamp + // MARK: ---- does not update the image update timestamp it("does not update the image update timestamp") { OpenGroupManager .roomImage( @@ -3359,7 +3329,7 @@ class OpenGroupManagerSpec: QuickSpec { }) } - // MARK: -- adds the image retrieval publisher to the cache + // MARK: ---- adds the image retrieval publisher to the cache it("adds the image retrieval publisher to the cache") { let publisher = OpenGroupManager .roomImage( @@ -3377,9 +3347,9 @@ class OpenGroupManagerSpec: QuickSpec { }) } - // MARK: -- for the default server + // MARK: ---- for the default server context("for the default server") { - // MARK: ---- fetches a new image if there is no cached one + // MARK: ------ fetches a new image if there is no cached one it("fetches a new image if there is no cached one") { var result: Data? @@ -3397,7 +3367,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(result).to(equal(Data([1, 2, 3]))) } - // MARK: ---- saves the fetched image to storage + // MARK: ------ saves the fetched image to storage it("saves the fetched image to storage") { OpenGroupManager .roomImage( @@ -3420,7 +3390,7 @@ class OpenGroupManagerSpec: QuickSpec { ).toNot(beNil()) } - // MARK: ---- updates the image update timestamp + // MARK: ------ updates the image update timestamp it("updates the image update timestamp") { OpenGroupManager .roomImage( @@ -3441,7 +3411,7 @@ class OpenGroupManagerSpec: QuickSpec { }) } - // MARK: ---- and there is a cached image + // MARK: ------ and there is a cached image context("and there is a cached image") { beforeEach { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) @@ -3460,7 +3430,7 @@ class OpenGroupManagerSpec: QuickSpec { }) } - // MARK: ------ retrieves the cached image + // MARK: -------- retrieves the cached image it("retrieves the cached image") { var result: Data? @@ -3478,7 +3448,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(result).to(equal(Data([2, 3, 4]))) } - // MARK: ------ fetches a new image if the cached on is older than a week + // MARK: -------- fetches a new image if the cached on is older than a week it("fetches a new image if the cached on is older than a week") { let weekInSeconds: TimeInterval = (7 * 24 * 60 * 60) let targetTimestamp: TimeInterval = ( diff --git a/SessionMessagingKitTests/Open Groups/Types/NonceGeneratorSpec.swift b/SessionMessagingKitTests/Open Groups/Types/NonceGeneratorSpec.swift index b7db2898f..ba1801ec9 100644 --- a/SessionMessagingKitTests/Open Groups/Types/NonceGeneratorSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Types/NonceGeneratorSpec.swift @@ -8,16 +8,18 @@ import Nimble @testable import SessionMessagingKit class NonceGeneratorSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - a NonceGenerator16Byte describe("a NonceGenerator16Byte") { + // MARK: -- has the correct number of bytes it("has the correct number of bytes") { expect(OpenGroupAPI.NonceGenerator16Byte().NonceBytes).to(equal(16)) } } + // MARK: - a NonceGenerator24Byte describe("a NonceGenerator24Byte") { + // MARK: -- has the correct number of bytes it("has the correct number of bytes") { expect(OpenGroupAPI.NonceGenerator24Byte().NonceBytes).to(equal(24)) } diff --git a/SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift b/SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift index f82ccaed0..ab6c201a1 100644 --- a/SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift @@ -8,10 +8,10 @@ import Nimble @testable import SessionMessagingKit class PersonalizationSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - a Personalization describe("a Personalization") { + // MARK: -- generates bytes correctly it("generates bytes correctly") { expect(OpenGroupAPI.Personalization.sharedKeys.bytes) .to(equal([115, 111, 103, 115, 46, 115, 104, 97, 114, 101, 100, 95, 107, 101, 121, 115])) diff --git a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift b/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift index 1dfb972d9..884b7473d 100644 --- a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift @@ -8,10 +8,10 @@ import Nimble @testable import SessionMessagingKit class SOGSEndpointSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - a SOGSEndpoint describe("a SOGSEndpoint") { + // MARK: -- generates the path value correctly it("generates the path value correctly") { // Utility diff --git a/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift b/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift index cba429cee..c2bc0307c 100644 --- a/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift @@ -8,10 +8,10 @@ import Nimble @testable import SessionMessagingKit class SOGSErrorSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - a SOGSError describe("a SOGSError") { + // MARK: -- generates the error description correctly it("generates the error description correctly") { expect(OpenGroupAPIError.decryptionFailed.errorDescription) .to(equal("Couldn't decrypt response.")) diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverDecryptionSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverDecryptionSpec.swift index 4429b0ecd..da3e5c94b 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverDecryptionSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverDecryptionSpec.swift @@ -11,45 +11,35 @@ import Nimble @testable import SessionMessagingKit class MessageReceiverDecryptionSpec: QuickSpec { - // MARK: - Spec - - override func spec() { - var mockStorage: Storage! - var mockCrypto: MockCrypto! - var dependencies: Dependencies! + override class func spec() { + // MARK: Configuration - describe("a MessageReceiver") { - beforeEach { - mockStorage = SynchronousStorage( - customWriter: try! DatabaseQueue(), - customMigrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ] - ) - mockCrypto = MockCrypto() - dependencies = Dependencies( - storage: mockStorage, - crypto: mockCrypto - ) - - mockStorage.write { db in - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - } - mockCrypto - .when { [dependencies = dependencies!] crypto in + @TestState var mockStorage: Storage! = SynchronousStorage( + customWriter: try! DatabaseQueue(), + customMigrationTargets: [ + SNUtilitiesKit.self, + SNMessagingKit.self + ], + initialData: { db in + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) + } + ) + @TestState var mockCrypto: MockCrypto! = MockCrypto( + initialSetup: { crypto in + crypto + .when { crypto in try crypto.perform( .encryptAeadXChaCha20( message: anyArray(), secretKey: anyArray(), nonce: anyArray(), - using: dependencies + using: any() ) ) } .thenReturn(nil) - mockCrypto + crypto .when { try $0.perform( .open( @@ -60,13 +50,13 @@ class MessageReceiverDecryptionSpec: QuickSpec { ) } .thenReturn([UInt8](repeating: 0, count: 100)) - mockCrypto - .when { [dependencies = dependencies!] crypto in + crypto + .when { crypto in crypto.generate( .blindedKeyPair( serverPublicKey: any(), edKeyPair: any(), - using: dependencies + using: any() ) ) } @@ -76,34 +66,36 @@ class MessageReceiverDecryptionSpec: QuickSpec { secretKey: Data(hex: TestConstants.edSecretKey).bytes ) ) - mockCrypto - .when { [dependencies = dependencies!] crypto in + crypto + .when { crypto in try crypto.perform( .sharedBlindedEncryptionKey( secretKey: anyArray(), otherBlindedPublicKey: anyArray(), fromBlindedPublicKey: anyArray(), toBlindedPublicKey: anyArray(), - using: dependencies + using: any() ) ) } .thenReturn([]) - mockCrypto - .when { [dependencies = dependencies!] crypto in - try crypto.perform(.generateBlindingFactor(serverPublicKey: any(), using: dependencies)) + crypto + .when { crypto in + try crypto.perform( + .generateBlindingFactor(serverPublicKey: any(), using: any()) + ) } .thenReturn([]) - mockCrypto + crypto .when { try $0.perform(.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray())) } .thenReturn(Data(hex: TestConstants.blindedPublicKey).bytes) - mockCrypto + crypto .when { try $0.perform(.toX25519(ed25519PublicKey: anyArray())) } .thenReturn(Data(hex: TestConstants.publicKey).bytes) - mockCrypto + crypto .when { $0.verify(.signature(message: anyArray(), publicKey: anyArray(), signature: anyArray())) } .thenReturn(true) - mockCrypto + crypto .when { try $0.perform( .decryptAeadXChaCha20( @@ -114,15 +106,24 @@ class MessageReceiverDecryptionSpec: QuickSpec { ) } .thenReturn("TestMessage".data(using: .utf8)!.bytes + [UInt8](repeating: 0, count: 32)) - mockCrypto.when { $0.size(.nonce24) }.thenReturn(24) - mockCrypto.when { $0.size(.publicKey) }.thenReturn(32) - mockCrypto.when { $0.size(.signature) }.thenReturn(64) - mockCrypto + crypto.when { $0.size(.nonce24) }.thenReturn(24) + crypto.when { $0.size(.publicKey) }.thenReturn(32) + crypto.when { $0.size(.signature) }.thenReturn(64) + crypto .when { try $0.perform(.generateNonce24()) } .thenReturn(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes) } - + ) + @TestState var dependencies: Dependencies! = Dependencies( + storage: mockStorage, + crypto: mockCrypto + ) + + // MARK: - a MessageReceiver + describe("a MessageReceiver") { + // MARK: -- when decrypting with the session protocol context("when decrypting with the session protocol") { + // MARK: ---- successfully decrypts a message it("successfully decrypts a message") { let result = try? MessageReceiver.decryptWithSessionProtocol( ciphertext: Data( @@ -142,6 +143,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(equal("0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) } + // MARK: ---- throws an error if it cannot open the message it("throws an error if it cannot open the message") { mockCrypto .when { @@ -168,6 +170,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(throwError(MessageReceiverError.decryptionFailed)) } + // MARK: ---- throws an error if the open message is too short it("throws an error if the open message is too short") { mockCrypto .when { @@ -194,6 +197,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(throwError(MessageReceiverError.decryptionFailed)) } + // MARK: ---- throws an error if it cannot verify the message it("throws an error if it cannot verify the message") { mockCrypto .when { $0.verify(.signature(message: anyArray(), publicKey: anyArray(), signature: anyArray())) } @@ -212,6 +216,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(throwError(MessageReceiverError.invalidSignature)) } + // MARK: ---- throws an error if it cannot get the senders x25519 public key it("throws an error if it cannot get the senders x25519 public key") { mockCrypto.when { try $0.perform(.toX25519(ed25519PublicKey: anyArray())) }.thenReturn(nil) @@ -229,7 +234,9 @@ class MessageReceiverDecryptionSpec: QuickSpec { } } + // MARK: -- when decrypting with the blinded session protocol context("when decrypting with the blinded session protocol") { + // MARK: ---- successfully decrypts a message it("successfully decrypts a message") { let result = try? MessageReceiver.decryptWithSessionBlindingProtocol( data: Data( @@ -252,6 +259,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(equal("0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) } + // MARK: ---- successfully decrypts a mocked incoming message it("successfully decrypts a mocked incoming message") { let result = try? MessageReceiver.decryptWithSessionBlindingProtocol( data: ( @@ -274,6 +282,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(equal("0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) } + // MARK: ---- throws an error if the data is too short it("throws an error if the data is too short") { expect { try MessageReceiver.decryptWithSessionBlindingProtocol( @@ -291,6 +300,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(throwError(MessageReceiverError.decryptionFailed)) } + // MARK: ---- throws an error if it cannot get the blinded keyPair it("throws an error if it cannot get the blinded keyPair") { mockCrypto .when { [dependencies = dependencies!] crypto in @@ -324,6 +334,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(throwError(MessageReceiverError.decryptionFailed)) } + // MARK: ---- throws an error if it cannot get the decryption key it("throws an error if it cannot get the decryption key") { mockCrypto .when { [dependencies = dependencies!] crypto in @@ -359,6 +370,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(throwError(MessageReceiverError.decryptionFailed)) } + // MARK: ---- throws an error if the data version is not 0 it("throws an error if the data version is not 0") { expect { try MessageReceiver.decryptWithSessionBlindingProtocol( @@ -380,6 +392,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(throwError(MessageReceiverError.decryptionFailed)) } + // MARK: ---- throws an error if it cannot decrypt the data it("throws an error if it cannot decrypt the data") { mockCrypto .when { @@ -413,6 +426,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(throwError(MessageReceiverError.decryptionFailed)) } + // MARK: ---- throws an error if the inner bytes are too short it("throws an error if the inner bytes are too short") { mockCrypto .when { @@ -446,6 +460,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(throwError(MessageReceiverError.decryptionFailed)) } + // MARK: ---- throws an error if it cannot generate the blinding factor it("throws an error if it cannot generate the blinding factor") { mockCrypto .when { [dependencies = dependencies!] crypto in @@ -473,6 +488,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(throwError(MessageReceiverError.invalidSignature)) } + // MARK: ---- throws an error if it cannot generate the combined key it("throws an error if it cannot generate the combined key") { mockCrypto .when { try $0.perform(.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray())) } @@ -498,6 +514,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(throwError(MessageReceiverError.invalidSignature)) } + // MARK: ---- throws an error if the combined key does not match kA it("throws an error if the combined key does not match kA") { mockCrypto .when { try $0.perform(.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray())) } @@ -523,6 +540,7 @@ class MessageReceiverDecryptionSpec: QuickSpec { .to(throwError(MessageReceiverError.invalidSignature)) } + // MARK: ---- throws an error if it cannot get the senders x25519 public key it("throws an error if it cannot get the senders x25519 public key") { mockCrypto .when { try $0.perform(.toX25519(ed25519PublicKey: anyArray())) } diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderEncryptionSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderEncryptionSpec.swift index 27510a4f5..2f1b56c49 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderEncryptionSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderEncryptionSpec.swift @@ -11,41 +11,35 @@ import Nimble @testable import SessionMessagingKit class MessageSenderEncryptionSpec: QuickSpec { - // MARK: - Spec - - override func spec() { - var mockStorage: Storage! - var mockCrypto: MockCrypto! - var dependencies: Dependencies! + override class func spec() { + // MARK: Configuration - describe("a MessageSender") { - // MARK: - Configuration - - beforeEach { - mockStorage = SynchronousStorage( - customWriter: try! DatabaseQueue(), - customMigrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ] - ) - mockCrypto = MockCrypto() - - dependencies = Dependencies( - storage: mockStorage, - crypto: mockCrypto - ) - - mockStorage.write { db in - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - } - mockCrypto + @TestState var mockStorage: Storage! = SynchronousStorage( + customWriter: try! DatabaseQueue(), + customMigrationTargets: [ + SNUtilitiesKit.self, + SNMessagingKit.self + ], + initialData: { db in + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) + } + ) + @TestState var mockCrypto: MockCrypto! = MockCrypto( + initialSetup: { crypto in + crypto .when { try $0.perform(.generateNonce24()) } .thenReturn(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes) } - - // MARK: - when encrypting with the session protocol + ) + @TestState var dependencies: Dependencies! = Dependencies( + storage: mockStorage, + crypto: mockCrypto + ) + + // MARK: - a MessageSender + describe("a MessageSender") { + // MARK: -- when encrypting with the session protocol context("when encrypting with the session protocol") { beforeEach { mockCrypto @@ -56,7 +50,7 @@ class MessageSenderEncryptionSpec: QuickSpec { .thenReturn([]) } - // MARK: -- can encrypt correctly + // MARK: ---- can encrypt correctly it("can encrypt correctly") { let result: Data? = mockStorage.read { db in try? MessageSender.encryptWithSessionProtocol( @@ -72,7 +66,7 @@ class MessageSenderEncryptionSpec: QuickSpec { expect(result?.count).to(equal(155)) } - // MARK: -- returns the correct value when mocked + // MARK: ---- returns the correct value when mocked it("returns the correct value when mocked") { let result: Data? = mockStorage.read { db in try? MessageSender.encryptWithSessionProtocol( @@ -86,7 +80,7 @@ class MessageSenderEncryptionSpec: QuickSpec { expect(result?.bytes).to(equal([1, 2, 3])) } - // MARK: -- throws an error if there is no ed25519 keyPair + // MARK: ---- throws an error if there is no ed25519 keyPair it("throws an error if there is no ed25519 keyPair") { mockStorage.write { db in _ = try Identity.filter(id: .ed25519PublicKey).deleteAll(db) @@ -106,7 +100,7 @@ class MessageSenderEncryptionSpec: QuickSpec { } } - // MARK: -- throws an error if the signature generation fails + // MARK: ---- throws an error if the signature generation fails it("throws an error if the signature generation fails") { mockCrypto .when { try $0.perform(.signature(message: anyArray(), secretKey: anyArray())) } @@ -125,7 +119,7 @@ class MessageSenderEncryptionSpec: QuickSpec { } } - // MARK: -- throws an error if the encryption fails + // MARK: ---- throws an error if the encryption fails it("throws an error if the encryption fails") { mockCrypto .when { try $0.perform(.seal(message: anyArray(), recipientPublicKey: anyArray())) } @@ -145,7 +139,7 @@ class MessageSenderEncryptionSpec: QuickSpec { } } - // MARK: - when encrypting with the blinded session protocol + // MARK: -- when encrypting with the blinded session protocol context("when encrypting with the blinded session protocol") { beforeEach { mockCrypto @@ -186,7 +180,7 @@ class MessageSenderEncryptionSpec: QuickSpec { .thenReturn([2, 3, 4]) } - // MARK: -- can encrypt correctly + // MARK: ---- can encrypt correctly it("can encrypt correctly") { let result: Data? = mockStorage.read { db in try? MessageSender.encryptWithSessionBlindingProtocol( @@ -203,7 +197,7 @@ class MessageSenderEncryptionSpec: QuickSpec { expect(result?.count).to(equal(84)) } - // MARK: -- returns the correct value when mocked + // MARK: ---- returns the correct value when mocked it("returns the correct value when mocked") { let result: Data? = mockStorage.read { db in try? MessageSender.encryptWithSessionBlindingProtocol( @@ -219,7 +213,7 @@ class MessageSenderEncryptionSpec: QuickSpec { .to(equal("00020304a5b4d48b3ade4f4b2a2764762e5a2c7900f254bd91633b43")) } - // MARK: -- includes a version at the start of the encrypted value + // MARK: ---- includes a version at the start of the encrypted value it("includes a version at the start of the encrypted value") { let result: Data? = mockStorage.read { db in try? MessageSender.encryptWithSessionBlindingProtocol( @@ -234,7 +228,7 @@ class MessageSenderEncryptionSpec: QuickSpec { expect(result?.toHexString().prefix(2)).to(equal("00")) } - // MARK: -- includes the nonce at the end of the encrypted value + // MARK: ---- includes the nonce at the end of the encrypted value it("includes the nonce at the end of the encrypted value") { let maybeResult: Data? = mockStorage.read { db in try? MessageSender.encryptWithSessionBlindingProtocol( @@ -252,7 +246,7 @@ class MessageSenderEncryptionSpec: QuickSpec { .to(equal("pbTUizreT0sqJ2R2LloseQDyVL2RYztD")) } - // MARK: -- throws an error if the recipient isn't a blinded id + // MARK: ---- throws an error if the recipient isn't a blinded id it("throws an error if the recipient isn't a blinded id") { mockStorage.read { db in expect { @@ -268,7 +262,7 @@ class MessageSenderEncryptionSpec: QuickSpec { } } - // MARK: -- throws an error if there is no ed25519 keyPair + // MARK: ---- throws an error if there is no ed25519 keyPair it("throws an error if there is no ed25519 keyPair") { mockStorage.write { db in _ = try Identity.filter(id: .ed25519PublicKey).deleteAll(db) @@ -289,7 +283,7 @@ class MessageSenderEncryptionSpec: QuickSpec { } } - // MARK: -- throws an error if it fails to generate a blinded keyPair + // MARK: ---- throws an error if it fails to generate a blinded keyPair it("throws an error if it fails to generate a blinded keyPair") { mockCrypto .when { [dependencies = dependencies!] crypto in @@ -317,7 +311,7 @@ class MessageSenderEncryptionSpec: QuickSpec { } } - // MARK: -- throws an error if it fails to generate an encryption key + // MARK: ---- throws an error if it fails to generate an encryption key it("throws an error if it fails to generate an encryption key") { mockCrypto .when { [dependencies = dependencies!] crypto in @@ -347,7 +341,7 @@ class MessageSenderEncryptionSpec: QuickSpec { } } - // MARK: -- throws an error if it fails to encrypt + // MARK: ---- throws an error if it fails to encrypt it("throws an error if it fails to encrypt") { mockCrypto .when { diff --git a/SessionMessagingKitTests/Shared Models/SessionThreadViewModelSpec.swift b/SessionMessagingKitTests/Shared Models/SessionThreadViewModelSpec.swift index df58e26c3..4178346b0 100644 --- a/SessionMessagingKitTests/Shared Models/SessionThreadViewModelSpec.swift +++ b/SessionMessagingKitTests/Shared Models/SessionThreadViewModelSpec.swift @@ -9,57 +9,42 @@ import SessionUtilitiesKit @testable import SessionMessagingKit class SessionThreadViewModelSpec: QuickSpec { - public struct TestMessage: Codable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { - public static var databaseTableName: String { "testMessage" } + override class func spec() { + // MARK: Configuration - public typealias Columns = CodingKeys - public enum CodingKeys: String, CodingKey, ColumnExpression { - case body - } - - public let body: String - } - - // MARK: - Spec - - override func spec() { - describe("a SessionThreadViewModel") { - var mockStorage: Storage! - - beforeEach { - mockStorage = SynchronousStorage( - customWriter: try! DatabaseQueue() - ) + @TestState var mockStorage: Storage! = SynchronousStorage( + customWriter: try! DatabaseQueue(), + initialData: { db in + try db.create(table: TestMessage.self) { t in + t.column(.body, .text).notNull() + } - mockStorage.write { db in - try db.create(table: TestMessage.self) { t in - t.column(.body, .text).notNull() - } + try db.create(virtualTable: TestMessage.fullTextSearchTableName, using: FTS5()) { t in + t.synchronize(withTable: TestMessage.databaseTableName) + t.tokenizer = .porter(wrapping: .unicode61()) - try db.create(virtualTable: TestMessage.fullTextSearchTableName, using: FTS5()) { t in - t.synchronize(withTable: TestMessage.databaseTableName) - t.tokenizer = .porter(wrapping: .unicode61()) - - t.column(TestMessage.Columns.body.name) - } + t.column(TestMessage.Columns.body.name) } } - - // MARK: - when processing a search term + ) + + // MARK: - a SessionThreadViewModel + describe("a SessionThreadViewModel") { + // MARK: -- when processing a search term context("when processing a search term") { - // MARK: -- correctly generates a safe search term + // MARK: ---- correctly generates a safe search term it("correctly generates a safe search term") { expect(SessionThreadViewModel.searchSafeTerm("Test")).to(equal("\"Test\"")) } - // MARK: -- standardises odd quote characters + // MARK: ---- standardises odd quote characters it("standardises odd quote characters") { expect(SessionThreadViewModel.standardQuotes("\"")).to(equal("\"")) expect(SessionThreadViewModel.standardQuotes("”")).to(equal("\"")) expect(SessionThreadViewModel.standardQuotes("“")).to(equal("\"")) } - // MARK: -- splits on the space character + // MARK: ---- splits on the space character it("splits on the space character") { expect(SessionThreadViewModel.searchTermParts("Test Message")) .to(equal([ @@ -68,7 +53,7 @@ class SessionThreadViewModelSpec: QuickSpec { ])) } - // MARK: -- surrounds each split term with quotes + // MARK: ---- surrounds each split term with quotes it("surrounds each split term with quotes") { expect(SessionThreadViewModel.searchTermParts("Test Message")) .to(equal([ @@ -77,7 +62,7 @@ class SessionThreadViewModelSpec: QuickSpec { ])) } - // MARK: -- keeps words within quotes together + // MARK: ---- keeps words within quotes together it("keeps words within quotes together") { expect(SessionThreadViewModel.searchTermParts("This \"is a Test\" Message")) .to(equal([ @@ -113,7 +98,7 @@ class SessionThreadViewModelSpec: QuickSpec { ])) } - // MARK: -- keeps words within weird quotes together + // MARK: ---- keeps words within weird quotes together it("keeps words within weird quotes together") { expect(SessionThreadViewModel.searchTermParts("This ”is a Test“ Message")) .to(equal([ @@ -123,7 +108,7 @@ class SessionThreadViewModelSpec: QuickSpec { ])) } - // MARK: -- removes extra whitespace + // MARK: ---- removes extra whitespace it("removes extra whitespace") { expect(SessionThreadViewModel.searchTermParts(" Test Message ")) .to(equal([ @@ -133,7 +118,7 @@ class SessionThreadViewModelSpec: QuickSpec { } } - // MARK: - when searching + // MARK: -- when searching context("when searching") { beforeEach { mockStorage.write { db in @@ -156,7 +141,7 @@ class SessionThreadViewModelSpec: QuickSpec { } } - // MARK: -- returns results + // MARK: ---- returns results it("returns results") { let results = mockStorage.read { db in let pattern: FTS5Pattern = try SessionThreadViewModel.pattern( @@ -186,7 +171,7 @@ class SessionThreadViewModelSpec: QuickSpec { ])) } - // MARK: -- adds a wildcard to the final part + // MARK: ---- adds a wildcard to the final part it("adds a wildcard to the final part") { let results = mockStorage.read { db in let pattern: FTS5Pattern = try SessionThreadViewModel.pattern( @@ -216,7 +201,7 @@ class SessionThreadViewModelSpec: QuickSpec { ])) } - // MARK: -- does not add a wildcard to other parts + // MARK: ---- does not add a wildcard to other parts it("does not add a wildcard to other parts") { let results = mockStorage.read { db in let pattern: FTS5Pattern = try SessionThreadViewModel.pattern( @@ -239,7 +224,7 @@ class SessionThreadViewModelSpec: QuickSpec { .to(beEmpty()) } - // MARK: -- finds similar words without the wildcard due to the porter tokenizer + // MARK: ---- finds similar words without the wildcard due to the porter tokenizer it("finds similar words without the wildcard due to the porter tokenizer") { let results = mockStorage.read { db in let pattern: FTS5Pattern = try SessionThreadViewModel.pattern( @@ -271,7 +256,7 @@ class SessionThreadViewModelSpec: QuickSpec { ])) } - // MARK: -- finds results containing the words regardless of the order + // MARK: ---- finds results containing the words regardless of the order it("finds results containing the words regardless of the order") { let results = mockStorage.read { db in let pattern: FTS5Pattern = try SessionThreadViewModel.pattern( @@ -303,7 +288,7 @@ class SessionThreadViewModelSpec: QuickSpec { ])) } - // MARK: -- does not find quoted parts out of order + // MARK: ---- does not find quoted parts out of order it("does not find quoted parts out of order") { let results = mockStorage.read { db in let pattern: FTS5Pattern = try SessionThreadViewModel.pattern( @@ -332,3 +317,16 @@ class SessionThreadViewModelSpec: QuickSpec { } } } + +// MARK: - Test Types + +fileprivate struct TestMessage: Codable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { + public static var databaseTableName: String { "testMessage" } + + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression { + case body + } + + public let body: String +} diff --git a/SessionMessagingKitTests/Utilities/CryptoSMKSpec.swift b/SessionMessagingKitTests/Utilities/CryptoSMKSpec.swift index 5db9f95b9..b05eb335b 100644 --- a/SessionMessagingKitTests/Utilities/CryptoSMKSpec.swift +++ b/SessionMessagingKitTests/Utilities/CryptoSMKSpec.swift @@ -10,24 +10,18 @@ import Nimble @testable import SessionMessagingKit class CryptoSMKSpec: QuickSpec { - // MARK: - Spec - - override func spec() { - var crypto: Crypto! - var mockCrypto: MockCrypto! - var dependencies: Dependencies! + override class func spec() { + // MARK: Configuration - beforeEach { - crypto = Crypto() - mockCrypto = MockCrypto() - dependencies = Dependencies(crypto: crypto) - } + @TestState var crypto: Crypto! = Crypto() + @TestState var mockCrypto: MockCrypto! = MockCrypto() + @TestState var dependencies: Dependencies! = Dependencies(crypto: crypto) + // MARK: - Crypto for SessionMessagingKit describe("Crypto for SessionMessagingKit") { - - // MARK: - when extending Sign + // MARK: -- when extending Sign context("when extending Sign") { - // MARK: -- can convert an ed25519 public key into an x25519 public key + // MARK: ---- can convert an ed25519 public key into an x25519 public key it("can convert an ed25519 public key into an x25519 public key") { let result = try? crypto.perform(.toX25519(ed25519PublicKey: TestConstants.edPublicKey.bytes)) @@ -35,7 +29,7 @@ class CryptoSMKSpec: QuickSpec { .to(equal("95ffb559d4e804e9b414a5178454c426f616b4a61089b217b41165dbb7c9fe2d")) } - // MARK: -- can convert an ed25519 private key into an x25519 private key + // MARK: ---- can convert an ed25519 private key into an x25519 private key it("can convert an ed25519 private key into an x25519 private key") { let result = try? crypto.perform(.toX25519(ed25519SecretKey: TestConstants.edSecretKey.bytes)) @@ -44,11 +38,11 @@ class CryptoSMKSpec: QuickSpec { } } - // MARK: - when extending Sodium + // MARK: -- when extending Sodium context("when extending Sodium") { - // MARK: -- and generating a blinding factor + // MARK: ---- and generating a blinding factor context("and generating a blinding factor") { - // MARK: --- successfully generates a blinding factor + // MARK: ------ successfully generates a blinding factor it("successfully generates a blinding factor") { let result = try? crypto.perform( .generateBlindingFactor( @@ -61,7 +55,7 @@ class CryptoSMKSpec: QuickSpec { .to(equal("84e3eb75028a9b73fec031b7448e322a68ca6485fad81ab1bead56f759ebeb0f")) } - // MARK: --- fails if the serverPublicKey is not a hex string + // MARK: ------ fails if the serverPublicKey is not a hex string it("fails if the serverPublicKey is not a hex string") { let result = try? crypto.perform( .generateBlindingFactor( @@ -73,7 +67,7 @@ class CryptoSMKSpec: QuickSpec { expect(result).to(beNil()) } - // MARK: --- fails if it cannot hash the serverPublicKey bytes + // MARK: ------ fails if it cannot hash the serverPublicKey bytes it("fails if it cannot hash the serverPublicKey bytes") { dependencies = Dependencies(crypto: mockCrypto) mockCrypto @@ -91,9 +85,9 @@ class CryptoSMKSpec: QuickSpec { } } - // MARK: -- and generating a blinded key pair + // MARK: ---- and generating a blinded key pair context("and generating a blinded key pair") { - // MARK: --- successfully generates a blinded key pair + // MARK: ------ successfully generates a blinded key pair it("successfully generates a blinded key pair") { let result = crypto.generate( .blindedKeyPair( @@ -112,7 +106,7 @@ class CryptoSMKSpec: QuickSpec { .to(equal("16663322d6b684e1c9dcc02b9e8642c3affd3bc431a9ea9e63dbbac88ce7a305")) } - // MARK: --- fails if the edKeyPair public key length wrong + // MARK: ------ fails if the edKeyPair public key length wrong it("fails if the edKeyPair public key length wrong") { let result = crypto.generate( .blindedKeyPair( @@ -128,7 +122,7 @@ class CryptoSMKSpec: QuickSpec { expect(result).to(beNil()) } - // MARK: --- fails if the edKeyPair secret key length wrong + // MARK: ------ fails if the edKeyPair secret key length wrong it("fails if the edKeyPair secret key length wrong") { let result = crypto.generate( .blindedKeyPair( @@ -144,7 +138,7 @@ class CryptoSMKSpec: QuickSpec { expect(result).to(beNil()) } - // MARK: --- fails if it cannot generate a blinding factor + // MARK: ------ fails if it cannot generate a blinding factor it("fails if it cannot generate a blinding factor") { let result = crypto.generate( .blindedKeyPair( @@ -161,9 +155,9 @@ class CryptoSMKSpec: QuickSpec { } } - // MARK: -- and generating a sogsSignature + // MARK: ---- and generating a sogsSignature context("and generating a sogsSignature") { - // MARK: --- generates a correct signature + // MARK: ------ generates a correct signature it("generates a correct signature") { let result = try? crypto.perform( .sogsSignature( @@ -182,9 +176,9 @@ class CryptoSMKSpec: QuickSpec { } } - // MARK: -- and combining keys + // MARK: ---- and combining keys context("and combining keys") { - // MARK: --- generates a correct combined key + // MARK: ------ generates a correct combined key it("generates a correct combined key") { let result = try? crypto.perform( .combineKeys( @@ -198,9 +192,9 @@ class CryptoSMKSpec: QuickSpec { } } - // MARK: -- and creating a shared blinded encryption key + // MARK: ---- and creating a shared blinded encryption key context("and creating a shared blinded encryption key") { - // MARK: --- generates a correct combined key + // MARK: ------ generates a correct combined key it("generates a correct combined key") { let result = try? crypto.perform( .sharedBlindedEncryptionKey( @@ -216,7 +210,7 @@ class CryptoSMKSpec: QuickSpec { .to(equal("388ee09e4c356b91f1cce5cc0aa0cf59e8e8cade69af61685d09c2d2731bc99e")) } - // MARK: --- fails if the scalar multiplication fails + // MARK: ------ fails if the scalar multiplication fails it("fails if the scalar multiplication fails") { let result = try? crypto.perform( .sharedBlindedEncryptionKey( @@ -232,9 +226,9 @@ class CryptoSMKSpec: QuickSpec { } } - // MARK: -- and checking if a session id matches a blinded id + // MARK: ---- and checking if a session id matches a blinded id context("and checking if a session id matches a blinded id") { - // MARK: --- returns true when they match + // MARK: ------ returns true when they match it("returns true when they match") { let result = crypto.verify( .sessionId( @@ -248,7 +242,7 @@ class CryptoSMKSpec: QuickSpec { expect(result).to(beTrue()) } - // MARK: --- returns false if given an invalid session id + // MARK: ------ returns false if given an invalid session id it("returns false if given an invalid session id") { let result = crypto.verify( .sessionId( @@ -262,7 +256,7 @@ class CryptoSMKSpec: QuickSpec { expect(result).to(beFalse()) } - // MARK: --- returns false if given an invalid blinded id + // MARK: ------ returns false if given an invalid blinded id it("returns false if given an invalid blinded id") { let result = crypto.verify( .sessionId( @@ -276,7 +270,7 @@ class CryptoSMKSpec: QuickSpec { expect(result).to(beFalse()) } - // MARK: --- returns false if it fails to generate the blinding factor + // MARK: ------ returns false if it fails to generate the blinding factor it("returns false if it fails to generate the blinding factor") { let result = crypto.verify( .sessionId( @@ -292,11 +286,11 @@ class CryptoSMKSpec: QuickSpec { } } - // MARK: - when extending GenericHash + // MARK: -- when extending GenericHash describe("when extending GenericHash") { - // MARK: -- and generating a hash with salt and personal values + // MARK: ---- and generating a hash with salt and personal values context("and generating a hash with salt and personal values") { - // MARK: --- generates a hash correctly + // MARK: ------ generates a hash correctly it("generates a hash correctly") { let result = try? crypto.perform( .hashSaltPersonal( @@ -312,7 +306,7 @@ class CryptoSMKSpec: QuickSpec { expect(result?.count).to(equal(32)) } - // MARK: --- generates a hash correctly with no key + // MARK: ------ generates a hash correctly with no key it("generates a hash correctly with no key") { let result = try? crypto.perform( .hashSaltPersonal( @@ -328,7 +322,7 @@ class CryptoSMKSpec: QuickSpec { expect(result?.count).to(equal(32)) } - // MARK: --- fails if given invalid options + // MARK: ------ fails if given invalid options it("fails if given invalid options") { let result = try? crypto.perform( .hashSaltPersonal( @@ -345,11 +339,11 @@ class CryptoSMKSpec: QuickSpec { } } - // MARK: - when extending AeadXChaCha20Poly1305Ietf + // MARK: -- when extending AeadXChaCha20Poly1305Ietf context("when extending AeadXChaCha20Poly1305Ietf") { - // MARK: -- when encrypting + // MARK: ---- when encrypting context("when encrypting") { - // MARK: --- encrypts correctly + // MARK: ------ encrypts correctly it("encrypts correctly") { let result = try? crypto.perform( .encryptAeadXChaCha20( @@ -365,7 +359,7 @@ class CryptoSMKSpec: QuickSpec { expect(result?.count).to(equal(27)) } - // MARK: --- encrypts correctly with additional data + // MARK: ------ encrypts correctly with additional data it("encrypts correctly with additional data") { let result = try? crypto.perform( .encryptAeadXChaCha20( @@ -381,7 +375,7 @@ class CryptoSMKSpec: QuickSpec { expect(result?.count).to(equal(27)) } - // MARK: --- fails if given an invalid key + // MARK: ------ fails if given an invalid key it("fails if given an invalid key") { let result = try? crypto.perform( .encryptAeadXChaCha20( diff --git a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift index 6f2e5fbc3..60d648fdb 100644 --- a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift @@ -11,77 +11,58 @@ import SessionUtilitiesKit @testable import Session class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec { - typealias ParentType = SessionTableViewModel - - // MARK: - Spec - - override func spec() { - var mockStorage: Storage! - var cancellables: [AnyCancellable] = [] - var dependencies: Dependencies! - var viewModel: ThreadDisappearingMessagesSettingsViewModel! + override class func spec() { + // MARK: Configuration + @TestState var mockStorage: Storage! = SynchronousStorage( + customWriter: try! DatabaseQueue(), + customMigrationTargets: [ + SNUtilitiesKit.self, + SNSnodeKit.self, + SNMessagingKit.self, + SNUIKit.self + ], + initialData: { db in + try SessionThread( + id: "TestId", + variant: .contact + ).insert(db) + } + ) + @TestState var dependencies: Dependencies! = Dependencies( + storage: mockStorage, + scheduler: .immediate + ) + @TestState var viewModel: ThreadDisappearingMessagesSettingsViewModel! = ThreadDisappearingMessagesSettingsViewModel( + threadId: "TestId", + threadVariant: .contact, + config: DisappearingMessagesConfiguration.defaultWith("TestId"), + using: dependencies + ) + + @TestState var cancellables: [AnyCancellable]! = [ + viewModel.observableTableData + .receive(on: ImmediateScheduler.shared) + .sink( + receiveCompletion: { _ in }, + receiveValue: { viewModel.updateTableData($0.0) } + ) + ] + + // MARK: - a ThreadDisappearingMessagesSettingsViewModel describe("a ThreadDisappearingMessagesSettingsViewModel") { - // MARK: - Configuration - - beforeEach { - mockStorage = SynchronousStorage( - customWriter: try! DatabaseQueue(), - customMigrationTargets: [ - SNUtilitiesKit.self, - SNSnodeKit.self, - SNMessagingKit.self, - SNUIKit.self - ] - ) - dependencies = Dependencies( - storage: mockStorage, - scheduler: .immediate - ) - mockStorage.write { db in - try SessionThread( - id: "TestId", - variant: .contact - ).insert(db) - } - viewModel = ThreadDisappearingMessagesSettingsViewModel( - threadId: "TestId", - threadVariant: .contact, - config: DisappearingMessagesConfiguration.defaultWith("TestId"), - using: dependencies - ) - cancellables.append( - viewModel.observableTableData - .receive(on: ImmediateScheduler.shared) - .sink( - receiveCompletion: { _ in }, - receiveValue: { viewModel.updateTableData($0.0) } - ) - ) - } - - afterEach { - cancellables.forEach { $0.cancel() } - - mockStorage = nil - cancellables = [] - dependencies = nil - viewModel = nil - } - - // MARK: - Basic Tests - + // MARK: -- has the correct title it("has the correct title") { expect(viewModel.title).to(equal("DISAPPEARING_MESSAGES".localized())) } + // MARK: -- has the correct number of items it("has the correct number of items") { - expect(viewModel.tableData.count) - .to(equal(1)) - expect(viewModel.tableData.first?.elements.count) - .to(equal(12)) + expect(viewModel.tableData.count).to(equal(1)) + expect(viewModel.tableData.first?.elements.count).to(equal(12)) } + // MARK: -- has the correct default state it("has the correct default state") { expect(viewModel.tableData.first?.elements.first) .to( @@ -117,6 +98,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec { ) } + // MARK: -- starts with the correct item active if not default it("starts with the correct item active if not default") { let config: DisappearingMessagesConfiguration = DisappearingMessagesConfiguration .defaultWith("TestId") @@ -176,6 +158,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec { ) } + // MARK: -- has no right bar button it("has no right bar button") { var items: [ParentType.NavItem]? @@ -191,8 +174,9 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec { expect(items).to(equal([])) } + // MARK: -- when changed from the previous setting context("when changed from the previous setting") { - var items: [ParentType.NavItem]? + @TestState var items: [ParentType.NavItem]? beforeEach { cancellables.append( @@ -207,6 +191,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec { viewModel.tableData.first?.elements.last?.onTap?() } + // MARK: ---- shows the save button it("shows the save button") { expect(items) .to(equal([ @@ -218,7 +203,9 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec { ])) } + // MARK: ---- and saving context("and saving") { + // MARK: ------ dismisses the screen it("dismisses the screen") { var didDismissScreen: Bool = false @@ -236,6 +223,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec { expect(didDismissScreen).to(beTrue()) } + // MARK: ------ saves the updated config it("saves the updated config") { items?.first?.action?() @@ -252,3 +240,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec { } } } + +// MARK: - Test Types + +fileprivate typealias ParentType = SessionTableViewModel diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index e84e52368..93fb30727 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -11,97 +11,65 @@ import SessionUtilitiesKit @testable import Session class ThreadSettingsViewModelSpec: QuickSpec { - typealias ParentType = SessionTableViewModel - - // MARK: - Spec - - override func spec() { - var mockStorage: Storage! - var mockCaches: MockCaches! - var mockGeneralCache: MockGeneralCache! - var disposables: [AnyCancellable] = [] - var dependencies: Dependencies! - var viewModel: ThreadSettingsViewModel! - var didTriggerSearchCallbackTriggered: Bool = false + override class func spec() { + // MARK: Configuration - describe("a ThreadSettingsViewModel") { - // MARK: - Configuration - - beforeEach { - mockStorage = SynchronousStorage( - customWriter: try! DatabaseQueue(), - customMigrationTargets: [ - SNUtilitiesKit.self, - SNSnodeKit.self, - SNMessagingKit.self, - SNUIKit.self - ] - ) - mockCaches = MockCaches() - mockGeneralCache = MockGeneralCache() - dependencies = Dependencies( - storage: mockStorage, - caches: mockCaches, - scheduler: .immediate - ) - mockCaches[.general] = mockGeneralCache - mockGeneralCache.when { $0.encodedPublicKey }.thenReturn("05\(TestConstants.publicKey)") - mockStorage.write { db in - try SessionThread( - id: "TestId", - variant: .contact - ).insert(db) - - try Identity( - variant: .x25519PublicKey, - data: Data(hex: TestConstants.publicKey) - ).insert(db) - - try Profile( - id: "05\(TestConstants.publicKey)", - name: "TestMe", - lastNameUpdate: 0, - lastProfilePictureUpdate: 0 - ).insert(db) - - try Profile( - id: "TestId", - name: "TestUser", - lastNameUpdate: 0, - lastProfilePictureUpdate: 0 - ).insert(db) - } - viewModel = ThreadSettingsViewModel( - threadId: "TestId", - threadVariant: .contact, - didTriggerSearch: { - didTriggerSearchCallbackTriggered = true - }, - using: dependencies - ) - disposables.append( - viewModel.observableTableData - .receive(on: ImmediateScheduler.shared) - .sink( - receiveCompletion: { _ in }, - receiveValue: { viewModel.updateTableData($0.0) } - ) - ) - } - - afterEach { - disposables.forEach { $0.cancel() } + @TestState var mockStorage: Storage! = SynchronousStorage( + customWriter: try! DatabaseQueue(), + customMigrationTargets: [ + SNUtilitiesKit.self, + SNSnodeKit.self, + SNMessagingKit.self, + SNUIKit.self + ], + initialData: { db in + try Identity( + variant: .x25519PublicKey, + data: Data(hex: TestConstants.publicKey) + ).insert(db) - mockStorage = nil - disposables = [] - dependencies = nil - viewModel = nil - didTriggerSearchCallbackTriggered = false + try SessionThread(id: "TestId",variant: .contact).insert(db) + try Profile(id: "05\(TestConstants.publicKey)", name: "TestMe").insert(db) + try Profile(id: "TestId", name: "TestUser").insert(db) } - - // MARK: - Basic Tests - + ) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache( + initialSetup: { cache in + cache.when { $0.encodedPublicKey }.thenReturn("05\(TestConstants.publicKey)") + } + ) + @TestState var mockCaches: MockCaches! = MockCaches() + .setting(cache: .general, to: mockGeneralCache) + @TestState var dependencies: Dependencies! = Dependencies( + storage: mockStorage, + caches: mockCaches, + scheduler: .immediate + ) + @TestState var threadVariant: SessionThread.Variant! = .contact + @TestState var didTriggerSearchCallbackTriggered: Bool! = false + @TestState var viewModel: ThreadSettingsViewModel! = ThreadSettingsViewModel( + threadId: "TestId", + threadVariant: .contact, + didTriggerSearch: { + didTriggerSearchCallbackTriggered = true + }, + using: dependencies + ) + + @TestState var disposables: [AnyCancellable]! = [ + viewModel.observableTableData + .receive(on: ImmediateScheduler.shared) + .sink( + receiveCompletion: { _ in }, + receiveValue: { viewModel.updateTableData($0.0) } + ) + ] + + // MARK: - a ThreadSettingsViewModel + describe("a ThreadSettingsViewModel") { + // MARK: -- with any conversation type context("with any conversation type") { + // MARK: ---- triggers the search callback when tapping search it("triggers the search callback when tapping search") { viewModel.tableData .first(where: { $0.model == .content })? @@ -112,6 +80,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { expect(didTriggerSearchCallbackTriggered).to(beTrue()) } + // MARK: ---- mutes a conversation it("mutes a conversation") { viewModel.tableData .first(where: { $0.model == .content })? @@ -127,6 +96,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { .toNot(beNil()) } + // MARK: ---- unmutes a conversation it("unmutes a conversation") { mockStorage.write { db in try SessionThread @@ -158,6 +128,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { } } + // MARK: -- with a note-to-self conversation context("with a note-to-self conversation") { beforeEach { mockStorage.write { db in @@ -187,10 +158,12 @@ class ThreadSettingsViewModelSpec: QuickSpec { ) } + // MARK: ---- has the correct title it("has the correct title") { expect(viewModel.title).to(equal("vc_settings_title".localized())) } + // MARK: ---- starts in the standard nav state it("starts in the standard nav state") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) @@ -206,6 +179,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { ])) } + // MARK: ---- has no mute button it("has no mute button") { expect( viewModel.tableData @@ -215,6 +189,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { ).to(beNil()) } + // MARK: ---- when entering edit mode context("when entering edit mode") { beforeEach { viewModel.navState.sinkAndStore(in: &disposables) @@ -222,6 +197,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { viewModel.textChanged("TestNew", for: .nickname) } + // MARK: ------ enters the editing state it("enters the editing state") { expect(viewModel.navState.firstValue()) .to(equal(.editing)) @@ -244,11 +220,13 @@ class ThreadSettingsViewModelSpec: QuickSpec { ])) } + // MARK: ------ when cancelling edit mode context("when cancelling edit mode") { beforeEach { viewModel.leftNavItems.firstValue()??.first?.action?() } + // MARK: -------- exits editing mode it("exits editing mode") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) @@ -264,6 +242,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { ])) } + // MARK: -------- does not update the nickname for the current user it("does not update the nickname for the current user") { expect( mockStorage @@ -276,11 +255,13 @@ class ThreadSettingsViewModelSpec: QuickSpec { } } + // MARK: ------ when saving edit mode context("when saving edit mode") { beforeEach { viewModel.rightNavItems.firstValue()??.first?.action?() } + // MARK: -------- exits editing mode it("exits editing mode") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) @@ -296,6 +277,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { ])) } + // MARK: -------- updates the nickname for the current user it("updates the nickname for the current user") { expect( mockStorage @@ -310,6 +292,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { } } + // MARK: -- with a one-to-one conversation context("with a one-to-one conversation") { beforeEach { mockStorage.write { db in @@ -322,10 +305,12 @@ class ThreadSettingsViewModelSpec: QuickSpec { } } + // MARK: ---- has the correct title it("has the correct title") { expect(viewModel.title).to(equal("vc_settings_title".localized())) } + // MARK: ---- starts in the standard nav state it("starts in the standard nav state") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) @@ -341,6 +326,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { ])) } + // MARK: ---- when entering edit mode context("when entering edit mode") { beforeEach { viewModel.navState.sinkAndStore(in: &disposables) @@ -348,6 +334,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { viewModel.textChanged("TestUserNew", for: .nickname) } + // MARK: ------ enters the editing state it("enters the editing state") { expect(viewModel.navState.firstValue()) .to(equal(.editing)) @@ -370,11 +357,13 @@ class ThreadSettingsViewModelSpec: QuickSpec { ])) } + // MARK: ------ when cancelling edit mode context("when cancelling edit mode") { beforeEach { viewModel.leftNavItems.firstValue()??.first?.action?() } + // MARK: -------- exits editing mode it("exits editing mode") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) @@ -390,6 +379,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { ])) } + // MARK: -------- does not update the nickname for the current user it("does not update the nickname for the current user") { expect( mockStorage @@ -400,11 +390,13 @@ class ThreadSettingsViewModelSpec: QuickSpec { } } + // MARK: ------ when saving edit mode context("when saving edit mode") { beforeEach { viewModel.rightNavItems.firstValue()??.first?.action?() } + // MARK: -------- exits editing mode it("exits editing mode") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) @@ -420,6 +412,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { ])) } + // MARK: -------- updates the nickname for the current user it("updates the nickname for the current user") { expect( mockStorage @@ -432,6 +425,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { } } + // MARK: -- with a group conversation context("with a group conversation") { beforeEach { mockStorage.write { db in @@ -461,10 +455,12 @@ class ThreadSettingsViewModelSpec: QuickSpec { ) } + // MARK: ---- has the correct title it("has the correct title") { expect(viewModel.title).to(equal("vc_group_settings_title".localized())) } + // MARK: ---- starts in the standard nav state it("starts in the standard nav state") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) @@ -474,6 +470,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { } } + // MARK: -- with a community conversation context("with a community conversation") { beforeEach { mockStorage.write { db in @@ -503,10 +500,12 @@ class ThreadSettingsViewModelSpec: QuickSpec { ) } + // MARK: ---- has the correct title it("has the correct title") { expect(viewModel.title).to(equal("vc_group_settings_title".localized())) } + // MARK: ---- starts in the standard nav state it("starts in the standard nav state") { expect(viewModel.navState.firstValue()) .to(equal(.standard)) @@ -518,3 +517,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { } } } + +// MARK: - Test Types + +fileprivate typealias ParentType = SessionTableViewModel diff --git a/SessionTests/Settings/NotificationContentViewModelSpec.swift b/SessionTests/Settings/NotificationContentViewModelSpec.swift index 090ea3d61..6925a22ee 100644 --- a/SessionTests/Settings/NotificationContentViewModelSpec.swift +++ b/SessionTests/Settings/NotificationContentViewModelSpec.swift @@ -11,52 +11,38 @@ import SessionUtilitiesKit @testable import Session class NotificationContentViewModelSpec: QuickSpec { - // MARK: - Spec - - override func spec() { - var mockStorage: Storage! - var dataChangeCancellable: AnyCancellable? - var dismissCancellable: AnyCancellable? - var viewModel: NotificationContentViewModel! + override class func spec() { + // MARK: Configuration + @TestState var mockStorage: Storage! = SynchronousStorage( + customWriter: try! DatabaseQueue(), + customMigrationTargets: [ + SNUtilitiesKit.self, + SNSnodeKit.self, + SNMessagingKit.self, + SNUIKit.self + ] + ) + @TestState var viewModel: NotificationContentViewModel! = NotificationContentViewModel( + storage: mockStorage, + scheduling: .immediate + ) + @TestState var dataChangeCancellable: AnyCancellable? = viewModel.observableTableData + .receive(on: ImmediateScheduler.shared) + .sink( + receiveCompletion: { _ in }, + receiveValue: { viewModel.updateTableData($0.0) } + ) + @TestState var dismissCancellable: AnyCancellable? + + // MARK: - a NotificationContentViewModel describe("a NotificationContentViewModel") { - // MARK: - Configuration - - beforeEach { - mockStorage = SynchronousStorage( - customWriter: try! DatabaseQueue(), - customMigrationTargets: [ - SNUtilitiesKit.self, - SNSnodeKit.self, - SNMessagingKit.self, - SNUIKit.self - ] - ) - viewModel = NotificationContentViewModel(storage: mockStorage, scheduling: .immediate) - dataChangeCancellable = viewModel.observableTableData - .receive(on: ImmediateScheduler.shared) - .sink( - receiveCompletion: { _ in }, - receiveValue: { viewModel.updateTableData($0.0) } - ) - } - - afterEach { - dataChangeCancellable?.cancel() - dismissCancellable?.cancel() - - mockStorage = nil - dataChangeCancellable = nil - dismissCancellable = nil - viewModel = nil - } - - // MARK: - Basic Tests - + // MARK: -- has the correct title it("has the correct title") { expect(viewModel.title).to(equal("NOTIFICATIONS_STYLE_CONTENT_TITLE".localized())) } + // MARK: -- has the correct number of items it("has the correct number of items") { expect(viewModel.tableData.count) .to(equal(1)) @@ -64,6 +50,7 @@ class NotificationContentViewModelSpec: QuickSpec { .to(equal(3)) } + // MARK: -- has the correct default state it("has the correct default state") { expect(viewModel.tableData.first?.elements) .to( @@ -96,6 +83,7 @@ class NotificationContentViewModelSpec: QuickSpec { ) } + // MARK: -- starts with the correct item active if not default it("starts with the correct item active if not default") { mockStorage.write { db in db[.preferencesNotificationPreviewType] = Preferences.NotificationPreviewType.nameNoPreview @@ -139,7 +127,9 @@ class NotificationContentViewModelSpec: QuickSpec { ) } + // MARK: -- when tapping an item context("when tapping an item") { + // MARK: ---- updates the saved preference it("updates the saved preference") { viewModel.tableData.first?.elements.last?.onTap?() @@ -147,6 +137,7 @@ class NotificationContentViewModelSpec: QuickSpec { .to(equal(Preferences.NotificationPreviewType.noNameNoPreview)) } + // MARK: ---- dismisses the screen it("dismisses the screen") { var didDismissScreen: Bool = false diff --git a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift index 3a93e06a9..d255064d5 100644 --- a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift +++ b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift @@ -9,21 +9,19 @@ import Nimble @testable import SessionUtilitiesKit class IdentitySpec: QuickSpec { - // MARK: - Spec - - override func spec() { - var mockStorage: Storage! + override class func spec() { + // MARK: Configuration + @TestState var mockStorage: Storage! = SynchronousStorage( + customWriter: try! DatabaseQueue(), + customMigrationTargets: [ + SNUtilitiesKit.self + ] + ) + + // MARK: - an Identity describe("an Identity") { - beforeEach { - mockStorage = SynchronousStorage( - customWriter: try! DatabaseQueue(), - customMigrationTargets: [ - SNUtilitiesKit.self - ] - ) - } - + // MARK: -- correctly retrieves the user user public key it("correctly retrieves the user user public key") { mockStorage.write { db in try Identity(variant: .x25519PublicKey, data: "Test1".data(using: .utf8)!).insert(db) @@ -35,6 +33,7 @@ class IdentitySpec: QuickSpec { } } + // MARK: -- correctly retrieves the user private key it("correctly retrieves the user private key") { mockStorage.write { db in try Identity(variant: .x25519PrivateKey, data: "Test2".data(using: .utf8)!).insert(db) @@ -46,6 +45,7 @@ class IdentitySpec: QuickSpec { } } + // MARK: -- correctly retrieves the user key pair it("correctly retrieves the user key pair") { mockStorage.write { db in try Identity(variant: .x25519PublicKey, data: "Test3".data(using: .utf8)!).insert(db) @@ -62,6 +62,7 @@ class IdentitySpec: QuickSpec { } } + // MARK: -- correctly determines if the user exists it("correctly determines if the user exists") { mockStorage.write { db in try Identity(variant: .x25519PublicKey, data: "Test3".data(using: .utf8)!).insert(db) @@ -74,6 +75,7 @@ class IdentitySpec: QuickSpec { } } + // MARK: -- correctly retrieves the user ED25519 key pair it("correctly retrieves the user ED25519 key pair") { mockStorage.write { db in try Identity(variant: .ed25519PublicKey, data: "Test5".data(using: .utf8)!).insert(db) @@ -90,6 +92,7 @@ class IdentitySpec: QuickSpec { } } + // MARK: -- correctly retrieves the hex encoded seed it("correctly retrieves the hex encoded seed") { mockStorage.write { db in try Identity(variant: .seed, data: "Test7".data(using: .utf8)!).insert(db) diff --git a/SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift b/SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift index 6b98ed4f4..2886f9db4 100644 --- a/SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift +++ b/SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift @@ -9,113 +9,22 @@ import Nimble @testable import SessionUtilitiesKit class PersistableRecordUtilitiesSpec: QuickSpec { - struct TestType: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { - public static var databaseTableName: String { "TestType" } + override class func spec() { + // MARK: Configuration - public typealias Columns = CodingKeys - public enum CodingKeys: String, CodingKey, ColumnExpression { - case columnA - case columnB - } - - public let columnA: String - public let columnB: String? - } - - struct MutableTestType: Codable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { - public static var databaseTableName: String { "MutableTestType" } - - public typealias Columns = CodingKeys - public enum CodingKeys: String, CodingKey, ColumnExpression { - case id - case columnA - case columnB - } - - public var id: Int64? - public let columnA: String - public let columnB: String? - - init(id: Int64? = nil, columnA: String, columnB: String?) { - self.id = id - self.columnA = columnA - self.columnB = columnB - } - - mutating func didInsert(_ inserted: InsertionSuccess) { - self.id = inserted.rowID - } - } - - enum TestInsertTestTypeMigration: Migration { - static let target: TargetMigrations.Identifier = .test - static let identifier: String = "TestInsertTestType" - static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0 - - static func migrate(_ db: Database) throws { - try db.create(table: TestType.self) { t in - t.column(.columnA, .text).primaryKey() - } - - try db.create(table: MutableTestType.self) { t in - t.column(.id, .integer).primaryKey(autoincrement: true) - t.column(.columnA, .text).unique() - } - } - } - - enum TestAddColumnMigration: Migration { - static let target: TargetMigrations.Identifier = .test - static let identifier: String = "TestAddColumn" - static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0 - - static func migrate(_ db: Database) throws { - try db.alter(table: TestType.self) { t in - t.add(.columnB, .text) - } - - try db.alter(table: MutableTestType.self) { t in - t.add(.columnB, .text) - } - } - } - - private struct TestTarget: MigratableTarget { - static func migrations(_ db: Database) -> TargetMigrations { - return TargetMigrations( - identifier: .test, - migrations: (0..<100) - .map { _ in [] } - .appending([TestInsertTestTypeMigration.self]) - ) - } - } - - // MARK: - Spec - - override func spec() { - var customWriter: DatabaseQueue! - var mockStorage: Storage! + @TestState var customWriter: DatabaseQueue! = try! DatabaseQueue() + @TestState var mockStorage: Storage! = SynchronousStorage( + customWriter: customWriter, + customMigrationTargets: [ + TestTarget.self + ] + ) + // MARK: - a PersistableRecord describe("a PersistableRecord") { - beforeEach { - customWriter = try! DatabaseQueue() - mockStorage = SynchronousStorage( - customWriter: customWriter, - customMigrationTargets: [ - TestTarget.self - ] - ) - } - - afterEach { - customWriter = nil - mockStorage = nil - } - + // MARK: -- before running the add column migration context("before running the add column migration") { + // MARK: ---- fails when using the standard insert it("fails when using the standard insert") { mockStorage.write { db in expect { @@ -125,6 +34,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- fails when using the standard inserted it("fails when using the standard inserted") { mockStorage.write { db in expect { @@ -134,6 +44,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- fails when using the standard save and the item does not already exist it("fails when using the standard save and the item does not already exist") { mockStorage.write { db in expect { @@ -143,6 +54,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- fails when using the standard saved and the item does not already exist it("fails when using the standard saved and the item does not already exist") { mockStorage.write { db in expect { @@ -152,6 +64,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- fails when using the standard upsert and the item does not already exist it("fails when using the standard upsert and the item does not already exist") { mockStorage.write { db in expect { @@ -161,6 +74,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- fails when using the standard mutable upsert and the item does not already exist it("fails when using the standard mutable upsert and the item does not already exist") { mockStorage.write { db in expect { @@ -172,6 +86,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- fails when using the standard upsert and the item already exists it("fails when using the standard upsert and the item already exists") { mockStorage.write { db in expect { @@ -185,6 +100,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- fails when using the standard mutable upsert and the item already exists it("fails when using the standard mutable upsert and the item already exists") { mockStorage.write { db in expect { @@ -200,6 +116,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe insert it("succeeds when using the migration safe insert") { mockStorage.write { db in expect { @@ -214,6 +131,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe inserted it("succeeds when using the migration safe inserted") { mockStorage.write { db in expect { @@ -235,6 +153,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe save and the item does not already exist it("succeeds when using the migration safe save and the item does not already exist") { mockStorage.write { db in expect { @@ -244,6 +163,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe saved and the item does not already exist it("succeeds when using the migration safe saved and the item does not already exist") { mockStorage.write { db in expect { @@ -265,6 +185,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe upsert and the item does not already exist it("succeeds when using the migration safe upsert and the item does not already exist") { mockStorage.write { db in expect { @@ -274,6 +195,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe mutable upsert and the item does not already exist it("succeeds when using the migration safe mutable upsert and the item does not already exist") { mockStorage.write { db in expect { @@ -290,8 +212,9 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } - // Note: The built-in 'update' method only updates existing columns so this shouldn't fail + // MARK: ---- succeeds when using the standard save and the item already exists it("succeeds when using the standard save and the item already exists") { + /// **Note:** The built-in 'update' method only updates existing columns so this shouldn't fail mockStorage.write { db in expect { try db.execute( @@ -304,10 +227,10 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } - // Note: The built-in 'update' method only updates existing columns so this won't fail - // due to the structure discrepancy but won't update the id as that only happens on - // insert + // MARK: ---- succeeds when using the standard saved and the item already exists it("succeeds when using the standard saved and the item already exists") { + /// **Note:** The built-in 'update' method only updates existing columns so this won't fail + /// due to the structure discrepancy but won't update the id as that only happens on insert mockStorage.write { db in expect { try db.execute( @@ -339,6 +262,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: -- after running the add column migration context("after running the add column migration") { beforeEach { var migrator: DatabaseMigrator = DatabaseMigrator() @@ -352,6 +276,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { .toNot(throwError()) } + // MARK: ---- succeeds when using the standard insert it("succeeds when using the standard insert") { mockStorage.write { db in expect { @@ -366,6 +291,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the standard inserted it("succeeds when using the standard inserted") { mockStorage.write { db in expect { @@ -380,6 +306,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the standard save and the item does not already exist it("succeeds when using the standard save and the item does not already exist") { mockStorage.write { db in expect { @@ -389,6 +316,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the standard saved and the item does not already exist it("succeeds when using the standard saved and the item does not already exist") { mockStorage.write { db in expect { @@ -398,6 +326,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the standard save and the item already exists it("succeeds when using the standard save and the item already exists") { mockStorage.write { db in expect { @@ -411,9 +340,9 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } - // Note: The built-in 'update' method won't update the id as that only happens on - // insert + // MARK: ---- succeeds when using the standard saved and the item already exists it("succeeds when using the standard saved and the item already exists") { + /// **Note:** The built-in 'update' method won't update the id as that only happens on insert mockStorage.write { db in expect { try db.execute( @@ -444,6 +373,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the standard upsert and the item does not already exist it("succeeds when using the standard upsert and the item does not already exist") { mockStorage.write { db in expect { @@ -453,6 +383,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the standard mutable upsert and the item does not already exist it("succeeds when using the standard mutable upsert and the item does not already exist") { mockStorage.write { db in expect { @@ -464,6 +395,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the standard upsert and the item already exists it("succeeds when using the standard upsert and the item already exists") { mockStorage.write { db in expect { @@ -477,9 +409,9 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } - // Note: The built-in 'update' method won't update the id as that only happens on - // insert + // MARK: ---- succeeds when using the standard mutable upsert and the item already exists it("succeeds when using the standard mutable upsert and the item already exists") { + /// **Note:** The built-in 'update' method won't update the id as that only happens on insert mockStorage.write { db in expect { try db.execute( @@ -512,6 +444,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe insert it("succeeds when using the migration safe insert") { mockStorage.write { db in expect { @@ -526,6 +459,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe inserted it("succeeds when using the migration safe inserted") { mockStorage.write { db in expect { @@ -547,6 +481,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe save and the item does not already exist it("succeeds when using the migration safe save and the item does not already exist") { mockStorage.write { db in expect { @@ -556,6 +491,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe saved and the item does not already exist it("succeeds when using the migration safe saved and the item does not already exist") { mockStorage.write { db in expect { @@ -565,6 +501,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe save and the item already exists it("succeeds when using the migration safe save and the item already exists") { mockStorage.write { db in expect { @@ -578,9 +515,9 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } - // Note: The built-in 'update' method won't update the id as that only happens on - // insert + // MARK: ---- succeeds when using the migration safe saved and the item already exists it("succeeds when using the migration safe saved and the item already exists") { + /// **Note:** The built-in 'update' method won't update the id as that only happens on insert mockStorage.write { db in expect { try db.execute( @@ -612,6 +549,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe upsert and the item does not already exist it("succeeds when using the migration safe upsert and the item does not already exist") { mockStorage.write { db in expect { @@ -621,6 +559,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe mutable upsert and the item does not already exist it("succeeds when using the migration safe mutable upsert and the item does not already exist") { mockStorage.write { db in expect { @@ -632,6 +571,7 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + // MARK: ---- succeeds when using the migration safe upsert and the item already exists it("succeeds when using the migration safe upsert and the item already exists") { mockStorage.write { db in expect { @@ -645,9 +585,9 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } - // Note: The built-in 'update' method won't update the id as that only happens on - // insert + // MARK: ---- succeeds when using the migration safe mutable upsert and the item already exists it("succeeds when using the migration safe mutable upsert and the item already exists") { + /// **Note:** The built-in 'update' method won't update the id as that only happens on insert mockStorage.write { db in expect { try db.execute( @@ -683,3 +623,89 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } } + +// MARK: - Test Types + +fileprivate struct TestType: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { + public static var databaseTableName: String { "TestType" } + + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression { + case columnA + case columnB + } + + public let columnA: String + public let columnB: String? +} + +fileprivate struct MutableTestType: Codable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { + public static var databaseTableName: String { "MutableTestType" } + + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression { + case id + case columnA + case columnB + } + + public var id: Int64? + public let columnA: String + public let columnB: String? + + init(id: Int64? = nil, columnA: String, columnB: String?) { + self.id = id + self.columnA = columnA + self.columnB = columnB + } + + mutating func didInsert(_ inserted: InsertionSuccess) { + self.id = inserted.rowID + } +} + +fileprivate enum TestInsertTestTypeMigration: Migration { + static let target: TargetMigrations.Identifier = .test + static let identifier: String = "TestInsertTestType" + static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0 + + static func migrate(_ db: Database) throws { + try db.create(table: TestType.self) { t in + t.column(.columnA, .text).primaryKey() + } + + try db.create(table: MutableTestType.self) { t in + t.column(.id, .integer).primaryKey(autoincrement: true) + t.column(.columnA, .text).unique() + } + } +} + +fileprivate enum TestAddColumnMigration: Migration { + static let target: TargetMigrations.Identifier = .test + static let identifier: String = "TestAddColumn" + static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0 + + static func migrate(_ db: Database) throws { + try db.alter(table: TestType.self) { t in + t.add(.columnB, .text) + } + + try db.alter(table: MutableTestType.self) { t in + t.add(.columnB, .text) + } + } +} + +fileprivate struct TestTarget: MigratableTarget { + static func migrations(_ db: Database) -> TargetMigrations { + return TargetMigrations( + identifier: .test, + migrations: (0..<100) + .map { _ in [] } + .appending([TestInsertTestTypeMigration.self]) + ) + } +} diff --git a/SessionUtilitiesKitTests/General/ArrayUtilitiesSpec.swift b/SessionUtilitiesKitTests/General/ArrayUtilitiesSpec.swift index b2d9b74dd..701fbb034 100644 --- a/SessionUtilitiesKitTests/General/ArrayUtilitiesSpec.swift +++ b/SessionUtilitiesKitTests/General/ArrayUtilitiesSpec.swift @@ -8,16 +8,12 @@ import Nimble @testable import SessionUtilitiesKit class ArrayUtilitiesSpec: QuickSpec { - private struct TestType: Equatable { - let stringValue: String - let intValue: Int - } - - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - an Array describe("an Array") { + // MARK: -- when grouping context("when grouping") { + // MARK: ---- maintains the original array ordering it("maintains the original array ordering") { let data: [TestType] = [ TestType(stringValue: "b", intValue: 5), @@ -84,3 +80,10 @@ class ArrayUtilitiesSpec: QuickSpec { } } } + +// MARK: - Test Types + +fileprivate struct TestType: Equatable { + let stringValue: String + let intValue: Int +} diff --git a/SessionUtilitiesKitTests/General/DependenciesSpec.swift b/SessionUtilitiesKitTests/General/DependenciesSpec.swift index 60b0d0ed3..aa8c89386 100644 --- a/SessionUtilitiesKitTests/General/DependenciesSpec.swift +++ b/SessionUtilitiesKitTests/General/DependenciesSpec.swift @@ -8,17 +8,16 @@ import Nimble @testable import SessionUtilitiesKit class DependenciesSpec: QuickSpec { - // MARK: - Spec - - override func spec() { - var dependencies: Dependencies! + override class func spec() { + // MARK: Configuration + @TestState var dependencies: Dependencies! = Dependencies() + + // MARK: - Dependencies describe("Dependencies") { - beforeEach { - dependencies = Dependencies() - } - + // MARK: -- when accessing dateNow context("when accessing dateNow") { + // MARK: ---- creates a new date every time when not overwritten it("creates a new date every time when not overwritten") { let date1 = dependencies.dateNow Thread.sleep(forTimeInterval: 0.05) @@ -27,6 +26,7 @@ class DependenciesSpec: QuickSpec { expect(date1.timeIntervalSince1970).toNot(equal(date2.timeIntervalSince1970)) } + // MARK: ---- returns the same new date every time when overwritten it("returns the same new date every time when overwritten") { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) diff --git a/SessionUtilitiesKitTests/General/SessionIdSpec.swift b/SessionUtilitiesKitTests/General/SessionIdSpec.swift index 62a76e33c..8f4896546 100644 --- a/SessionUtilitiesKitTests/General/SessionIdSpec.swift +++ b/SessionUtilitiesKitTests/General/SessionIdSpec.swift @@ -8,12 +8,14 @@ import Nimble @testable import SessionUtilitiesKit class SessionIdSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - a SessionId describe("a SessionId") { + // MARK: -- when initializing context("when initializing") { + // MARK: ---- with an idString context("with an idString") { + // MARK: ------ succeeds when correct it("succeeds when correct") { let sessionId: SessionId? = SessionId(from: "05\(TestConstants.publicKey)") @@ -21,16 +23,20 @@ class SessionIdSpec: QuickSpec { expect(sessionId?.publicKey).to(equal(TestConstants.publicKey)) } + // MARK: ------ fails when too short it("fails when too short") { expect(SessionId(from: "")).to(beNil()) } + // MARK: ------ fails with an invalid prefix it("fails with an invalid prefix") { expect(SessionId(from: "AB\(TestConstants.publicKey)")).to(beNil()) } } + // MARK: ---- with a prefix and publicKey context("with a prefix and publicKey") { + // MARK: ------ converts the bytes into a hex string it("converts the bytes into a hex string") { let sessionId: SessionId? = SessionId(.standard, publicKey: [0, 1, 2, 3, 4, 5, 6, 7, 8]) @@ -40,6 +46,7 @@ class SessionIdSpec: QuickSpec { } } + // MARK: -- generates the correct hex string it("generates the correct hex string") { expect(SessionId(.unblinded, publicKey: Data(hex: TestConstants.publicKey).bytes).hexString) .to(equal("0088672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) @@ -52,9 +59,13 @@ class SessionIdSpec: QuickSpec { } } + // MARK: - a SessionId Prefix describe("a SessionId Prefix") { + // MARK: -- when initializing context("when initializing") { + // MARK: ---- with just a prefix context("with just a prefix") { + // MARK: ------ succeeds when valid it("succeeds when valid") { expect(SessionId.Prefix(from: "00")).to(equal(.unblinded)) expect(SessionId.Prefix(from: "05")).to(equal(.standard)) @@ -62,24 +73,30 @@ class SessionIdSpec: QuickSpec { expect(SessionId.Prefix(from: "25")).to(equal(.blinded25)) } + // MARK: ------ fails when nil it("fails when nil") { expect(SessionId.Prefix(from: nil)).to(beNil()) } + // MARK: ------ fails when invalid it("fails when invalid") { expect(SessionId.Prefix(from: "AB")).to(beNil()) } } + // MARK: ---- with a longer string context("with a longer string") { + // MARK: ------ fails with invalid hex it("fails with invalid hex") { expect(SessionId.Prefix(from: "Hello!!!")).to(beNil()) } + // MARK: ------ fails with the wrong length it("fails with the wrong length") { expect(SessionId.Prefix(from: String(TestConstants.publicKey.prefix(10)))).to(beNil()) } + // MARK: ------ fails with an invalid prefix it("fails with an invalid prefix") { expect(SessionId.Prefix(from: "AB\(TestConstants.publicKey)")).to(beNil()) } diff --git a/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift b/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift index fff8e4c15..c0c264471 100644 --- a/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift +++ b/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift @@ -9,165 +9,71 @@ import Nimble @testable import SessionUtilitiesKit class JobRunnerSpec: QuickSpec { - struct TestDetails: Codable { - enum ResultType: Codable { - case success - case failure - case permanentFailure - case deferred - } + override class func spec() { + // MARK: Configuration - public let result: ResultType - public let completeTime: Int - public let intValue: Int64 - public let stringValue: String - - init( - result: ResultType = .success, - completeTime: Int = 0, - intValue: Int64 = 100, - stringValue: String = "200" - ) { - self.result = result - self.completeTime = completeTime - self.intValue = intValue - self.stringValue = stringValue - } - } - - struct InvalidDetails: Codable { - func encode(to encoder: Encoder) throws { throw HTTPError.parsingFailed } - } - - enum TestJob: JobExecutor { - static let maxFailureCount: Int = 1 - static let requiresThreadId: Bool = false - static let requiresInteractionId: Bool = false - - static func run( - _ job: Job, - queue: DispatchQueue, - success: @escaping (Job, Bool, Dependencies) -> (), - failure: @escaping (Job, Error?, Bool, Dependencies) -> (), - deferred: @escaping (Job, Dependencies) -> (), - using dependencies: Dependencies - ) { - guard - let detailsData: Data = job.details, - let details: TestDetails = try? JSONDecoder().decode(TestDetails.self, from: detailsData) - else { return success(job, true, dependencies) } - - let completeJob: () -> () = { - // Need to increase the 'completeTime' and 'nextRunTimestamp' to prevent the job - // from immediately being run again or immediately completing afterwards - let updatedJob: Job = job - .with(nextRunTimestamp: TimeInterval(details.completeTime + 1)) - .with( - details: TestDetails( - result: details.result, - completeTime: (details.completeTime + 2), - intValue: details.intValue, - stringValue: details.stringValue - ) - )! - dependencies.storage.write { db in try _ = updatedJob.saved(db) } - - switch details.result { - case .success: success(job, true, dependencies) - case .failure: failure(job, nil, false, dependencies) - case .permanentFailure: failure(job, nil, true, dependencies) - case .deferred: deferred(updatedJob, dependencies) - } - } - - guard dependencies.fixedTime < details.completeTime else { - return queue.async(using: dependencies) { - completeJob() - } - } - - dependencies.asyncExecutions.appendTo(details.completeTime) { - queue.async(using: dependencies) { - completeJob() - } - } - } - } - - // MARK: - Spec - - override func spec() { - var jobRunner: JobRunnerType! - var job1: Job! - var job2: Job! - var mockStorage: Storage! - var dependencies: Dependencies! - - describe("a JobRunner") { - // MARK: - Configuration - - beforeEach { - mockStorage = SynchronousStorage( - customWriter: try! DatabaseQueue(), - customMigrationTargets: [ - SNUtilitiesKit.self - ] - ) - dependencies = Dependencies( - storage: mockStorage, - dateNow: Date(timeIntervalSince1970: 0), - forceSynchronous: true - ) - + @TestState var job1: Job! = Job( + id: 100, + failureCount: 0, + variant: .messageSend, + behaviour: .runOnce, + shouldBlock: false, + shouldSkipLaunchBecomeActive: false, + nextRunTimestamp: 0, + threadId: nil, + interactionId: nil, + details: nil + ) + @TestState var job2: Job! = Job( + id: 101, + failureCount: 0, + variant: .attachmentUpload, + behaviour: .runOnce, + shouldBlock: false, + shouldSkipLaunchBecomeActive: false, + nextRunTimestamp: 0, + threadId: nil, + interactionId: nil, + details: nil + ) + @TestState var mockStorage: Storage! = SynchronousStorage( + customWriter: try! DatabaseQueue(), + customMigrationTargets: [ + SNUtilitiesKit.self + ], + initialData: { db in // Migrations add jobs which we don't want so delete them - mockStorage.write { db in try Job.deleteAll(db) } - - job1 = Job( - id: 100, - failureCount: 0, - variant: .messageSend, - behaviour: .runOnce, - shouldBlock: false, - shouldSkipLaunchBecomeActive: false, - nextRunTimestamp: 0, - threadId: nil, - interactionId: nil, - details: nil - ) - job2 = Job( - id: 101, - failureCount: 0, - variant: .attachmentUpload, - behaviour: .runOnce, - shouldBlock: false, - shouldSkipLaunchBecomeActive: false, - nextRunTimestamp: 0, - threadId: nil, - interactionId: nil, - details: nil - ) - - jobRunner = JobRunner(isTestingJobRunner: true, using: dependencies) - jobRunner.setExecutor(TestJob.self, for: .messageSend) - jobRunner.setExecutor(TestJob.self, for: .attachmentUpload) - jobRunner.setExecutor(TestJob.self, for: .messageReceive) - - // Need to assign this to ensure it's used by nested dependencies - dependencies.jobRunner = jobRunner + try Job.deleteAll(db) } + ) + @TestState var dependencies: Dependencies! = Dependencies( + storage: mockStorage, + dateNow: Date(timeIntervalSince1970: 0), + forceSynchronous: true + ) + @TestState var jobRunner: JobRunnerType! = { + let result = JobRunner(isTestingJobRunner: true, using: dependencies) + result.setExecutor(TestJob.self, for: .messageSend) + result.setExecutor(TestJob.self, for: .attachmentUpload) + result.setExecutor(TestJob.self, for: .messageReceive) + // Need to assign this to ensure it's used by nested dependencies + dependencies.jobRunner = result + + return result + }() + + // MARK: - a JobRunner + describe("a JobRunner") { afterEach { /// We **must** set `fixedTime` to ensure we break any loops within the `TestJob` executor dependencies.fixedTime = Int.max jobRunner.stopAndClearPendingJobs() - jobRunner = nil - mockStorage = nil - dependencies = nil } - // MARK: - when configuring + // MARK: -- when configuring context("when configuring") { - // MARK: -- adds an executor correctly + // MARK: ---- adds an executor correctly it("adds an executor correctly") { job1 = Job( id: 101, @@ -222,28 +128,28 @@ class JobRunnerSpec: QuickSpec { } } - // MARK: -- when managing state + // MARK: ---- when managing state context("when managing state") { - // MARK: ---- by checking if a job is currently running + // MARK: ------ by checking if a job is currently running context("by checking if a job is currently running") { - // MARK: ------ returns false when not given a job + // MARK: -------- returns false when not given a job it("returns false when not given a job") { expect(jobRunner.isCurrentlyRunning(nil)).to(beFalse()) } - // MARK: ------ returns false when given a job that has not been persisted + // MARK: -------- returns false when given a job that has not been persisted it("returns false when given a job that has not been persisted") { job1 = Job(variant: .messageSend) expect(jobRunner.isCurrentlyRunning(job1)).to(beFalse()) } - // MARK: ------ returns false when given a job that is not running + // MARK: -------- returns false when given a job that is not running it("returns false when given a job that is not running") { expect(jobRunner.isCurrentlyRunning(job1)).to(beFalse()) } - // MARK: ------ returns true when given a non blocking job that is running + // MARK: -------- returns true when given a non blocking job that is running it("returns true when given a non blocking job that is running") { job1 = job1.with(details: TestDetails(completeTime: 1)) jobRunner.appDidFinishLaunching(using: dependencies) @@ -261,7 +167,7 @@ class JobRunnerSpec: QuickSpec { expect(jobRunner.isCurrentlyRunning(job1)).to(beTrue()) } - // MARK: ------ returns true when given a blocking job that is running + // MARK: -------- returns true when given a blocking job that is running it("returns true when given a blocking job that is running") { job2 = Job( id: 101, @@ -293,14 +199,14 @@ class JobRunnerSpec: QuickSpec { } } - // MARK: ---- by getting the details for jobs + // MARK: ------ by getting the details for jobs context("by getting the details for jobs") { - // MARK: ------ returns an empty dictionary when there are no jobs + // MARK: -------- returns an empty dictionary when there are no jobs it("returns an empty dictionary when there are no jobs") { expect(jobRunner.allJobInfo()).to(equal([:])) } - // MARK: ------ returns an empty dictionary when there are no jobs matching the filters + // MARK: -------- returns an empty dictionary when there are no jobs matching the filters it("returns an empty dictionary when there are no jobs matching the filters") { jobRunner.appDidFinishLaunching(using: dependencies) jobRunner.appDidBecomeActive(using: dependencies) @@ -317,7 +223,7 @@ class JobRunnerSpec: QuickSpec { expect(jobRunner.jobInfoFor(state: .running, variant: .messageSend)).to(equal([:])) } - // MARK: ------ can filter to specific jobs + // MARK: -------- can filter to specific jobs it("can filter to specific jobs") { job1 = Job( id: 100, @@ -386,7 +292,7 @@ class JobRunnerSpec: QuickSpec { ])) } - // MARK: ------ can filter to running jobs + // MARK: -------- can filter to running jobs it("can filter to running jobs") { job1 = Job( id: 100, @@ -450,7 +356,7 @@ class JobRunnerSpec: QuickSpec { expect(Array(jobRunner.allJobInfo().keys).sorted()).to(equal([100, 101])) } - // MARK: ------ can filter to pending jobs + // MARK: -------- can filter to pending jobs it("can filter to pending jobs") { job1 = Job( id: 100, @@ -514,7 +420,7 @@ class JobRunnerSpec: QuickSpec { expect(Array(jobRunner.allJobInfo().keys).sorted()).to(equal([100, 101])) } - // MARK: ------ can filter to specific variants + // MARK: -------- can filter to specific variants it("can filter to specific variants") { job1 = job1.with(details: TestDetails(completeTime: 1)) job2 = job2.with(details: TestDetails(completeTime: 2)) @@ -552,7 +458,7 @@ class JobRunnerSpec: QuickSpec { expect(Array(jobRunner.allJobInfo().keys).sorted()).to(equal([100, 101])) } - // MARK: ------ includes non blocking jobs + // MARK: -------- includes non blocking jobs it("includes non blocking jobs") { job2 = job2.with(details: TestDetails(completeTime: 1)) jobRunner.appDidFinishLaunching(using: dependencies) @@ -580,7 +486,7 @@ class JobRunnerSpec: QuickSpec { ])) } - // MARK: ------ includes blocking jobs + // MARK: -------- includes blocking jobs it("includes blocking jobs") { job2 = Job( id: 101, @@ -622,9 +528,9 @@ class JobRunnerSpec: QuickSpec { } } - // MARK: ---- by checking for an existing job + // MARK: ------ by checking for an existing job context("by checking for an existing job") { - // MARK: ------ returns false for a queue that doesn't exist + // MARK: -------- returns false for a queue that doesn't exist it("returns false for a queue that doesn't exist") { jobRunner = JobRunner( isTestingJobRunner: true, @@ -636,19 +542,19 @@ class JobRunnerSpec: QuickSpec { .to(beFalse()) } - // MARK: ------ returns false when the provided details fail to decode + // MARK: -------- returns false when the provided details fail to decode it("returns false when the provided details fail to decode") { expect(jobRunner.hasJob(of: .attachmentUpload, with: InvalidDetails())) .to(beFalse()) } - // MARK: ------ returns false when there is not a pending or running job + // MARK: -------- returns false when there is not a pending or running job it("returns false when there is not a pending or running job") { expect(jobRunner.hasJob(of: .attachmentUpload, with: TestDetails())) .to(beFalse()) } - // MARK: ------ returns true when there is a pending job + // MARK: -------- returns true when there is a pending job it("returns true when there is a pending job") { job1 = Job( id: 100, @@ -703,7 +609,7 @@ class JobRunnerSpec: QuickSpec { .to(beTrue()) } - // MARK: ------ returns true when there is a running job + // MARK: -------- returns true when there is a running job it("returns true when there is a running job") { job2 = job2.with(details: TestDetails(completeTime: 1)) jobRunner.appDidFinishLaunching(using: dependencies) @@ -724,7 +630,7 @@ class JobRunnerSpec: QuickSpec { .to(beTrue()) } - // MARK: ------ returns true when there is a blocking job + // MARK: -------- returns true when there is a blocking job it("returns true when there is a blocking job") { job2 = Job( id: 101, @@ -760,7 +666,7 @@ class JobRunnerSpec: QuickSpec { .to(beTrue()) } - // MARK: ------ returns true when there is a non blocking job + // MARK: -------- returns true when there is a non blocking job it("returns true when there is a non blocking job") { job2 = job2.with(details: TestDetails(completeTime: 1)) jobRunner.appDidFinishLaunching(using: dependencies) @@ -782,9 +688,9 @@ class JobRunnerSpec: QuickSpec { } } - // MARK: ---- by being notified of app launch + // MARK: ------ by being notified of app launch context("by being notified of app launch") { - // MARK: ------ does not start a job before getting the app launch call + // MARK: -------- does not start a job before getting the app launch call it("does not start a job before getting the app launch call") { job1 = job1.with(details: TestDetails(completeTime: 1)) @@ -800,7 +706,7 @@ class JobRunnerSpec: QuickSpec { expect(jobRunner.isCurrentlyRunning(job1)).to(beFalse()) } - // MARK: ------ starts the job queues if there are no app launch jobs + // MARK: -------- starts the job queues if there are no app launch jobs it("does nothing if there are no app launch jobs") { job1 = job1.with(details: TestDetails(completeTime: 1)) @@ -818,9 +724,9 @@ class JobRunnerSpec: QuickSpec { } } - // MARK: ---- by being notified of app becoming active + // MARK: ------ by being notified of app becoming active context("by being notified of app becoming active") { - // MARK: ------ does not start a job before getting the app active call + // MARK: -------- does not start a job before getting the app active call it("does not start a job before getting the app active call") { job1 = job1.with(details: TestDetails(completeTime: 1)) @@ -836,7 +742,7 @@ class JobRunnerSpec: QuickSpec { expect(jobRunner.isCurrentlyRunning(job1)).to(beFalse()) } - // MARK: ------ does not start the job queues if there are no app active jobs and blocking jobs are running + // MARK: -------- does not start the job queues if there are no app active jobs and blocking jobs are running it("does not start the job queues if there are no app active jobs and blocking jobs are running") { job1 = job1.with(details: TestDetails(completeTime: 2)) job2 = Job( @@ -881,7 +787,7 @@ class JobRunnerSpec: QuickSpec { expect(jobRunner.isCurrentlyRunning(job1)).to(beFalse()) } - // MARK: ------ does not start the job queues if there are app active jobs and blocking jobs are running + // MARK: -------- does not start the job queues if there are app active jobs and blocking jobs are running it("does not start the job queues if there are app active jobs and blocking jobs are running") { job1 = Job( id: 100, @@ -939,7 +845,7 @@ class JobRunnerSpec: QuickSpec { expect(jobRunner.isCurrentlyRunning(job1)).to(beFalse()) } - // MARK: ------ starts the job queues if there are no app active jobs + // MARK: -------- starts the job queues if there are no app active jobs it("starts the job queues if there are no app active jobs") { job1 = job1.with(details: TestDetails(completeTime: 1)) jobRunner.appDidFinishLaunching(using: dependencies) @@ -962,7 +868,7 @@ class JobRunnerSpec: QuickSpec { expect(jobRunner.isCurrentlyRunning(job1)).to(beTrue()) } - // MARK: ------ starts the job queues if there are app active jobs + // MARK: -------- starts the job queues if there are app active jobs it("starts the job queues if there are app active jobs") { job1 = Job( id: 100, @@ -1020,7 +926,7 @@ class JobRunnerSpec: QuickSpec { expect(jobRunner.isCurrentlyRunning(job2)).to(beTrue()) } - // MARK: ------ starts the job queues after completing blocking app launch jobs + // MARK: -------- starts the job queues after completing blocking app launch jobs it("starts the job queues after completing blocking app launch jobs") { job1 = job1.with(details: TestDetails(completeTime: 2)) job2 = Job( @@ -1074,7 +980,7 @@ class JobRunnerSpec: QuickSpec { expect(jobRunner.isCurrentlyRunning(job1)).to(beTrue()) } - // MARK: ------ starts the job queues alongside non blocking app launch jobs + // MARK: -------- starts the job queues alongside non blocking app launch jobs it("starts the job queues alongside non blocking app launch jobs") { job1 = job1.with(details: TestDetails(completeTime: 1)) job2 = Job( @@ -1120,9 +1026,9 @@ class JobRunnerSpec: QuickSpec { } } - // MARK: ---- by checking if a job can be added to the queue + // MARK: ------ by checking if a job can be added to the queue context("by checking if a job can be added to the queue") { - // MARK: ------ does not add a general job to the queue before launch + // MARK: -------- does not add a general job to the queue before launch it("does not add a general job to the queue before launch") { job1 = Job( id: 100, @@ -1151,7 +1057,7 @@ class JobRunnerSpec: QuickSpec { expect(jobRunner.allJobInfo()).to(beEmpty()) } - // MARK: ------ adds a launch job to the queue in a pending state before launch + // MARK: -------- adds a launch job to the queue in a pending state before launch it("adds a launch job to the queue in a pending state before launch") { job1 = Job( id: 100, @@ -1180,7 +1086,7 @@ class JobRunnerSpec: QuickSpec { expect(Array(jobRunner.jobInfoFor(state: [.pending]).keys)).to(equal([100])) } - // MARK: ------ does not add a general job to the queue after launch but before becoming active + // MARK: -------- does not add a general job to the queue after launch but before becoming active it("does not add a general job to the queue after launch but before becoming active") { job1 = Job( id: 100, @@ -1210,7 +1116,7 @@ class JobRunnerSpec: QuickSpec { expect(jobRunner.allJobInfo()).to(beEmpty()) } - // MARK: ------ adds a launch job to the queue in a pending state after launch but before becoming active + // MARK: -------- adds a launch job to the queue in a pending state after launch but before becoming active it("adds a launch job to the queue in a pending state after launch but before becoming active") { job1 = Job( id: 100, @@ -1240,7 +1146,7 @@ class JobRunnerSpec: QuickSpec { expect(Array(jobRunner.jobInfoFor(state: .pending).keys)).to(equal([100])) } - // MARK: ------ adds a general job to the queue after becoming active + // MARK: -------- adds a general job to the queue after becoming active it("adds a general job to the queue after becoming active") { job1 = Job( id: 100, @@ -1271,7 +1177,7 @@ class JobRunnerSpec: QuickSpec { expect(Array(jobRunner.allJobInfo().keys)).to(equal([100])) } - // MARK: ------ adds a launch job to the queue and starts it after becoming active + // MARK: -------- adds a launch job to the queue and starts it after becoming active it("adds a launch job to the queue and starts it after becoming active") { job1 = Job( id: 100, @@ -1304,16 +1210,16 @@ class JobRunnerSpec: QuickSpec { } } - // MARK: -- when running jobs + // MARK: ---- when running jobs context("when running jobs") { beforeEach { jobRunner.appDidFinishLaunching(using: dependencies) jobRunner.appDidBecomeActive(using: dependencies) } - // MARK: ---- by adding + // MARK: ------ by adding context("by adding") { - // MARK: ------ does not start until after the db transaction completes + // MARK: -------- does not start until after the db transaction completes it("does not start until after the db transaction completes") { job1 = job1.with(details: TestDetails(completeTime: 1)) @@ -1327,9 +1233,9 @@ class JobRunnerSpec: QuickSpec { } } - // MARK: ---- with job dependencies + // MARK: ------ with job dependencies context("with job dependencies") { - // MARK: ------ starts dependencies first + // MARK: -------- starts dependencies first it("starts dependencies first") { job1 = job1.with(details: TestDetails(completeTime: 1)) job2 = job2.with(details: TestDetails(completeTime: 2)) @@ -1347,7 +1253,7 @@ class JobRunnerSpec: QuickSpec { .to(equal([101])) } - // MARK: ------ removes the initial job from the queue + // MARK: -------- removes the initial job from the queue it("removes the initial job from the queue") { job1 = job1.with(details: TestDetails(completeTime: 1)) job2 = job2.with(details: TestDetails(completeTime: 2)) @@ -1366,7 +1272,7 @@ class JobRunnerSpec: QuickSpec { expect(jobRunner.jobInfoFor(state: .running, variant: .messageSend).keys).toNot(contain(100)) } - // MARK: ------ starts the initial job when the dependencies succeed + // MARK: -------- starts the initial job when the dependencies succeed it("starts the initial job when the dependencies succeed") { job1 = job1.with(details: TestDetails(completeTime: 2)) job2 = job2.with(details: TestDetails(completeTime: 1)) @@ -1390,7 +1296,7 @@ class JobRunnerSpec: QuickSpec { .to(equal([100])) } - // MARK: ------ does not start the initial job if the dependencies are deferred + // MARK: -------- does not start the initial job if the dependencies are deferred it("does not start the initial job if the dependencies are deferred") { job1 = job1.with(details: TestDetails(result: .failure, completeTime: 2)) job2 = job2.with(details: TestDetails(result: .deferred, completeTime: 1)) @@ -1413,7 +1319,7 @@ class JobRunnerSpec: QuickSpec { expect(Array(jobRunner.jobInfoFor(state: .running).keys)).to(beEmpty()) } - // MARK: ------ does not start the initial job if the dependencies fail + // MARK: -------- does not start the initial job if the dependencies fail it("does not start the initial job if the dependencies fail") { job1 = job1.with(details: TestDetails(result: .failure, completeTime: 2)) job2 = job2.with(details: TestDetails(result: .failure, completeTime: 1)) @@ -1436,7 +1342,7 @@ class JobRunnerSpec: QuickSpec { expect(Array(jobRunner.jobInfoFor(state: .running).keys)).to(beEmpty()) } - // MARK: ------ does not delete the initial job if the dependencies fail + // MARK: -------- does not delete the initial job if the dependencies fail it("does not delete the initial job if the dependencies fail") { job1 = job1.with(details: TestDetails(result: .failure, completeTime: 2)) job2 = job2.with(details: TestDetails(result: .failure, completeTime: 1)) @@ -1466,7 +1372,7 @@ class JobRunnerSpec: QuickSpec { expect(mockStorage.read { db in try Job.fetchCount(db) }).to(equal(2)) } - // MARK: ------ deletes the initial job if the dependencies permanently fail + // MARK: -------- deletes the initial job if the dependencies permanently fail it("deletes the initial job if the dependencies permanently fail") { job1 = job1.with(details: TestDetails(result: .failure, completeTime: 2)) job2 = job2.with(details: TestDetails(result: .permanentFailure, completeTime: 1)) @@ -1495,16 +1401,16 @@ class JobRunnerSpec: QuickSpec { } } - // MARK: -- when completing jobs + // MARK: ---- when completing jobs context("when completing jobs") { beforeEach { jobRunner.appDidFinishLaunching(using: dependencies) jobRunner.appDidBecomeActive(using: dependencies) } - // MARK: ---- by succeeding + // MARK: ------ by succeeding context("by succeeding") { - // MARK: ------ removes the job from the queue + // MARK: -------- removes the job from the queue it("removes the job from the queue") { job1 = job1.with(details: TestDetails(result: .success, completeTime: 1)) @@ -1522,7 +1428,7 @@ class JobRunnerSpec: QuickSpec { expect(Array(jobRunner.jobInfoFor(state: .running).keys)).to(beEmpty()) } - // MARK: ------ deletes the job + // MARK: -------- deletes the job it("deletes the job") { job1 = job1.with(details: TestDetails(result: .success, completeTime: 1)) @@ -1544,9 +1450,9 @@ class JobRunnerSpec: QuickSpec { } } - // MARK: ---- by deferring + // MARK: ------ by deferring context("by deferring") { - // MARK: ------ reschedules the job to run again later + // MARK: -------- reschedules the job to run again later it("reschedules the job to run again later") { job1 = job1.with(details: TestDetails(result: .deferred, completeTime: 1)) @@ -1571,7 +1477,7 @@ class JobRunnerSpec: QuickSpec { )) } - // MARK: ------ does not delete the job + // MARK: -------- does not delete the job it("does not delete the job") { job1 = job1.with(details: TestDetails(result: .deferred, completeTime: 1)) @@ -1592,7 +1498,7 @@ class JobRunnerSpec: QuickSpec { expect(mockStorage.read { db in try Job.fetchCount(db) }).toNot(equal(0)) } - // MARK: ------ fails the job if it is deferred too many times + // MARK: -------- fails the job if it is deferred too many times it("fails the job if it is deferred too many times") { job1 = job1.with(details: TestDetails(result: .deferred, completeTime: 1)) @@ -1666,9 +1572,9 @@ class JobRunnerSpec: QuickSpec { } } - // MARK: ---- by failing + // MARK: ------ by failing context("by failing") { - // MARK: ------ removes the job from the queue + // MARK: -------- removes the job from the queue it("removes the job from the queue") { job1 = job1.with(details: TestDetails(result: .failure, completeTime: 1)) @@ -1686,7 +1592,7 @@ class JobRunnerSpec: QuickSpec { expect(Array(jobRunner.jobInfoFor(state: .running).keys)).to(beEmpty()) } - // MARK: ------ does not delete the job + // MARK: -------- does not delete the job it("does not delete the job") { job1 = job1.with(details: TestDetails(result: .failure, completeTime: 1)) @@ -1708,9 +1614,9 @@ class JobRunnerSpec: QuickSpec { } } - // MARK: ---- by permanently failing + // MARK: ------ by permanently failing context("by permanently failing") { - // MARK: ------ removes the job from the queue + // MARK: -------- removes the job from the queue it("removes the job from the queue") { job1 = job1.with(details: TestDetails(result: .permanentFailure, completeTime: 1)) @@ -1728,7 +1634,7 @@ class JobRunnerSpec: QuickSpec { expect(Array(jobRunner.jobInfoFor(state: .running).keys)).to(beEmpty()) } - // MARK: ------ deletes the job + // MARK: -------- deletes the job it("deletes the job") { job1 = job1.with(details: TestDetails(result: .permanentFailure, completeTime: 1)) @@ -1753,3 +1659,90 @@ class JobRunnerSpec: QuickSpec { } } } + +// MARK: - Test Types + +fileprivate struct TestDetails: Codable { + enum ResultType: Codable { + case success + case failure + case permanentFailure + case deferred + } + + public let result: ResultType + public let completeTime: Int + public let intValue: Int64 + public let stringValue: String + + init( + result: ResultType = .success, + completeTime: Int = 0, + intValue: Int64 = 100, + stringValue: String = "200" + ) { + self.result = result + self.completeTime = completeTime + self.intValue = intValue + self.stringValue = stringValue + } +} + +fileprivate struct InvalidDetails: Codable { + func encode(to encoder: Encoder) throws { throw HTTPError.parsingFailed } +} + +fileprivate enum TestJob: JobExecutor { + static let maxFailureCount: Int = 1 + static let requiresThreadId: Bool = false + static let requiresInteractionId: Bool = false + + static func run( + _ job: Job, + queue: DispatchQueue, + success: @escaping (Job, Bool, Dependencies) -> (), + failure: @escaping (Job, Error?, Bool, Dependencies) -> (), + deferred: @escaping (Job, Dependencies) -> (), + using dependencies: Dependencies + ) { + guard + let detailsData: Data = job.details, + let details: TestDetails = try? JSONDecoder().decode(TestDetails.self, from: detailsData) + else { return success(job, true, dependencies) } + + let completeJob: () -> () = { + // Need to increase the 'completeTime' and 'nextRunTimestamp' to prevent the job + // from immediately being run again or immediately completing afterwards + let updatedJob: Job = job + .with(nextRunTimestamp: TimeInterval(details.completeTime + 1)) + .with( + details: TestDetails( + result: details.result, + completeTime: (details.completeTime + 2), + intValue: details.intValue, + stringValue: details.stringValue + ) + )! + dependencies.storage.write { db in try _ = updatedJob.saved(db) } + + switch details.result { + case .success: success(job, true, dependencies) + case .failure: failure(job, nil, false, dependencies) + case .permanentFailure: failure(job, nil, true, dependencies) + case .deferred: deferred(updatedJob, dependencies) + } + } + + guard dependencies.fixedTime < details.completeTime else { + return queue.async(using: dependencies) { + completeJob() + } + } + + dependencies.asyncExecutions.appendTo(details.completeTime) { + queue.async(using: dependencies) { + completeJob() + } + } + } +} diff --git a/SessionUtilitiesKitTests/Networking/BatchResponseSpec.swift b/SessionUtilitiesKitTests/Networking/BatchResponseSpec.swift index d3349a15a..5356d7fb4 100644 --- a/SessionUtilitiesKitTests/Networking/BatchResponseSpec.swift +++ b/SessionUtilitiesKitTests/Networking/BatchResponseSpec.swift @@ -9,21 +9,40 @@ import Nimble @testable import SessionUtilitiesKit class BatchResponseSpec: QuickSpec { - struct TestType: Codable, Equatable { - let stringValue: String - } - struct TestType2: Codable, Equatable { - let intValue: Int - let stringValue2: String - } - - // MARK: - Spec - - override func spec() { - // MARK: - HTTP.BatchSubResponse + override class func spec() { + // MARK: Configuration + @TestState var responseInfo: ResponseInfoType! = HTTP.ResponseInfo(code: 200, headers: [:]) + @TestState var testType: TestType! = TestType(stringValue: "test1") + @TestState var testType2: TestType2! = TestType2(intValue: 123, stringValue2: "test2") + @TestState var data: Data! = """ + [\([ + try! JSONEncoder().with(outputFormatting: .sortedKeys).encode( + HTTP.BatchSubResponse( + code: 200, + headers: [:], + body: testType, + failedToParseBody: false + ) + ), + try! JSONEncoder().with(outputFormatting: .sortedKeys).encode( + HTTP.BatchSubResponse( + code: 200, + headers: [:], + body: testType2, + failedToParseBody: false + ) + ) + ] + .map { String(data: $0, encoding: .utf8)! } + .joined(separator: ","))] + """.data(using: .utf8)! + + // MARK: - an HTTP.BatchSubResponse describe("an HTTP.BatchSubResponse") { + // MARK: -- when decoding context("when decoding") { + // MARK: ---- decodes correctly it("decodes correctly") { let jsonString: String = """ { @@ -45,6 +64,7 @@ class BatchResponseSpec: QuickSpec { expect(subResponse?.body).toNot(beNil()) } + // MARK: ---- decodes with invalid body data it("decodes with invalid body data") { let jsonString: String = """ { @@ -63,6 +83,7 @@ class BatchResponseSpec: QuickSpec { expect(subResponse).toNot(beNil()) } + // MARK: ---- flags invalid body data as invalid it("flags invalid body data as invalid") { let jsonString: String = """ { @@ -83,6 +104,7 @@ class BatchResponseSpec: QuickSpec { expect(subResponse?.failedToParseBody).to(beTrue()) } + // MARK: ---- does not flag a missing or invalid optional body as invalid it("does not flag a missing or invalid optional body as invalid") { let jsonString: String = """ { @@ -102,6 +124,7 @@ class BatchResponseSpec: QuickSpec { expect(subResponse?.failedToParseBody).to(beFalse()) } + // MARK: ---- does not flag a NoResponse body as invalid it("does not flag a NoResponse body as invalid") { let jsonString: String = """ { @@ -123,10 +146,9 @@ class BatchResponseSpec: QuickSpec { } } - // MARK: - Convenience - // MARK: --Decodable - + // MARK: - a Decodable describe("a Decodable") { + // MARK: -- decodes correctly it("decodes correctly") { let jsonData: Data = "{\"stringValue\":\"testValue\"}".data(using: .utf8)! let result: TestType? = try? TestType.decoded(from: jsonData) @@ -135,42 +157,9 @@ class BatchResponseSpec: QuickSpec { } } - // MARK: - --Combine - + // MARK: - a (ResponseInfoType, Data?) Publisher describe("a (ResponseInfoType, Data?) Publisher") { - var responseInfo: ResponseInfoType! - var testType: TestType! - var testType2: TestType2! - var data: Data! - - beforeEach { - responseInfo = HTTP.ResponseInfo(code: 200, headers: [:]) - testType = TestType(stringValue: "test1") - testType2 = TestType2(intValue: 123, stringValue2: "test2") - data = """ - [\([ - try! JSONEncoder().with(outputFormatting: .sortedKeys).encode( - HTTP.BatchSubResponse( - code: 200, - headers: [:], - body: testType, - failedToParseBody: false - ) - ), - try! JSONEncoder().with(outputFormatting: .sortedKeys).encode( - HTTP.BatchSubResponse( - code: 200, - headers: [:], - body: testType2, - failedToParseBody: false - ) - ) - ] - .map { String(data: $0, encoding: .utf8)! } - .joined(separator: ","))] - """.data(using: .utf8)! - } - + // MARK: -- decodes valid data correctly it("decodes valid data correctly") { var result: HTTP.BatchResponse? Just((responseInfo, data)) @@ -191,6 +180,7 @@ class BatchResponseSpec: QuickSpec { .to(equal(testType2)) } + // MARK: -- fails if there is no data it("fails if there is no data") { var error: Error? Just((responseInfo, nil)) @@ -203,6 +193,7 @@ class BatchResponseSpec: QuickSpec { expect(error).to(matchError(HTTPError.parsingFailed)) } + // MARK: -- fails if the data is not JSON it("fails if the data is not JSON") { var error: Error? Just((responseInfo, Data([1, 2, 3]))) @@ -215,6 +206,7 @@ class BatchResponseSpec: QuickSpec { expect(error).to(matchError(HTTPError.parsingFailed)) } + // MARK: -- fails if the data is not a JSON array it("fails if the data is not a JSON array") { var error: Error? Just((responseInfo, "{}".data(using: .utf8))) @@ -227,6 +219,7 @@ class BatchResponseSpec: QuickSpec { expect(error).to(matchError(HTTPError.parsingFailed)) } + // MARK: -- fails if the JSON array does not have the same number of items as the expected types it("fails if the JSON array does not have the same number of items as the expected types") { var error: Error? Just((responseInfo, data)) @@ -243,6 +236,7 @@ class BatchResponseSpec: QuickSpec { expect(error).to(matchError(HTTPError.parsingFailed)) } + // MARK: -- fails if one of the JSON array values fails to decode it("fails if one of the JSON array values fails to decode") { data = """ [\([ @@ -275,3 +269,13 @@ class BatchResponseSpec: QuickSpec { } } } + +// MARK: - Test Types + +fileprivate struct TestType: Codable, Equatable { + let stringValue: String +} +fileprivate struct TestType2: Codable, Equatable { + let intValue: Int + let stringValue2: String +} diff --git a/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift b/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift index 3ddbdc2a3..0ed525738 100644 --- a/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift +++ b/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift @@ -8,42 +8,12 @@ import Nimble @testable import SessionUtilitiesKit class BencodeSpec: QuickSpec { - struct TestType: Codable, Equatable { - let intValue: Int - let stringValue: String - } - - struct TestType2: Codable, Equatable { - let stringValue: String - let boolValue: Bool - } - - struct TestType3: Codable, Equatable { - let stringValue: String - let boolValue: Bool - - init(_ stringValue: String, _ boolValue: Bool) { - self.stringValue = stringValue - self.boolValue = boolValue - } - - init(from decoder: Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - - self = TestType3( - try container.decode(String.self, forKey: .stringValue), - ((try? container.decode(Bool.self, forKey: .boolValue)) ?? false) - ) - } - } - - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - Bencode describe("Bencode") { - // MARK: - when decoding + // MARK: -- when decoding context("when decoding") { - // MARK: -- should decode a basic string + // MARK: ---- should decode a basic string it("should decode a basic string") { let basicStringData: Data = "5:howdy".data(using: .utf8)! let result = try? Bencode.decode(String.self, from: basicStringData) @@ -51,7 +21,7 @@ class BencodeSpec: QuickSpec { expect(result).to(equal("howdy")) } - // MARK: -- should decode a basic integer + // MARK: ---- should decode a basic integer it("should decode a basic integer") { let basicIntegerData: Data = "i3e".data(using: .utf8)! let result = try? Bencode.decode(Int.self, from: basicIntegerData) @@ -59,7 +29,7 @@ class BencodeSpec: QuickSpec { expect(result).to(equal(3)) } - // MARK: -- should decode a list of integers + // MARK: ---- should decode a list of integers it("should decode a list of integers") { let basicIntListData: Data = "li1ei2ee".data(using: .utf8)! let result = try? Bencode.decode([Int].self, from: basicIntListData) @@ -67,7 +37,7 @@ class BencodeSpec: QuickSpec { expect(result).to(equal([1, 2])) } - // MARK: -- should decode a basic dict + // MARK: ---- should decode a basic dict it("should decode a basic dict") { let basicDictData: Data = "d4:spaml1:a1:bee".data(using: .utf8)! let result = try? Bencode.decode([String: [String]].self, from: basicDictData) @@ -75,7 +45,7 @@ class BencodeSpec: QuickSpec { expect(result).to(equal(["spam": ["a", "b"]])) } - // MARK: -- decodes a decodable type + // MARK: ---- decodes a decodable type it("decodes a decodable type") { let data: Data = "d8:intValuei100e11:stringValue4:Test".data(using: .utf8)! let result: TestType? = try? Bencode.decode(TestType.self, from: data) @@ -83,7 +53,7 @@ class BencodeSpec: QuickSpec { expect(result).to(equal(TestType(intValue: 100, stringValue: "Test"))) } - // MARK: -- decodes a stringified decodable type + // MARK: ---- decodes a stringified decodable type it("decodes a stringified decodable type") { let data: Data = "37:{\"intValue\":100,\"stringValue\":\"Test\"}".data(using: .utf8)! let result: TestType? = try? Bencode.decode(TestType.self, from: data) @@ -92,11 +62,11 @@ class BencodeSpec: QuickSpec { } } - // MARK: - when decoding a response + // MARK: -- when decoding a response context("when decoding a response") { - // MARK: -- with a decodable type + // MARK: ---- with a decodable type context("with a decodable type") { - // MARK: ---- decodes successfully + // MARK: ------ decodes successfully it("decodes successfully") { let data: Data = "ld8:intValuei100e11:stringValue4:Teste5:\u{01}\u{02}\u{03}\u{04}\u{05}e" .data(using: .utf8)! @@ -114,7 +84,7 @@ class BencodeSpec: QuickSpec { )) } - // MARK: -- decodes successfully with no body + // MARK: ------ decodes successfully with no body it("decodes successfully with no body") { let data: Data = "ld8:intValuei100e11:stringValue4:Teste" .data(using: .utf8)! @@ -132,7 +102,7 @@ class BencodeSpec: QuickSpec { )) } - // MARK: ---- throws a parsing error when given an invalid length + // MARK: ------ throws a parsing error when given an invalid length it("throws a parsing error when given an invalid length") { let data: Data = "ld12:intValuei100e11:stringValue4:Teste5:\u{01}\u{02}\u{03}\u{04}\u{05}e" .data(using: .utf8)! @@ -143,7 +113,7 @@ class BencodeSpec: QuickSpec { }.to(throwError(HTTPError.parsingFailed)) } - // MARK: ---- throws a parsing error when given an invalid key + // MARK: ------ throws a parsing error when given an invalid key it("throws a parsing error when given an invalid key") { let data: Data = "ld7:INVALIDi100e11:stringValue4:Teste5:\u{01}\u{02}\u{03}\u{04}\u{05}e" .data(using: .utf8)! @@ -154,7 +124,7 @@ class BencodeSpec: QuickSpec { }.to(throwError(HTTPError.parsingFailed)) } - // MARK: ---- decodes correctly when trying to decode an int to a bool with custom handling + // MARK: ------ decodes correctly when trying to decode an int to a bool with custom handling it("decodes correctly when trying to decode an int to a bool with custom handling") { let data: Data = "ld9:boolValuei1e11:stringValue4:testee" .data(using: .utf8)! @@ -165,7 +135,7 @@ class BencodeSpec: QuickSpec { }.toNot(throwError(HTTPError.parsingFailed)) } - // MARK: ---- throws a parsing error when trying to decode an int to a bool + // MARK: ------ throws a parsing error when trying to decode an int to a bool it("throws a parsing error when trying to decode an int to a bool") { let data: Data = "ld9:boolValuei1e11:stringValue4:testee" .data(using: .utf8)! @@ -177,9 +147,9 @@ class BencodeSpec: QuickSpec { } } - // MARK: -- with stringified json info + // MARK: ---- with stringified json info context("with stringified json info") { - // MARK: -- decodes successfully + // MARK: ------ decodes successfully it("decodes successfully") { let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e" .data(using: .utf8)! @@ -197,7 +167,7 @@ class BencodeSpec: QuickSpec { )) } - // MARK: -- decodes successfully with no body + // MARK: ------ decodes successfully with no body it("decodes successfully with no body") { let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}e" .data(using: .utf8)! @@ -215,7 +185,7 @@ class BencodeSpec: QuickSpec { )) } - // MARK: -- throws a parsing error when invalid + // MARK: ------ throws a parsing error when invalid it("throws a parsing error when invalid") { let data: Data = "l36:{\"INVALID\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e" .data(using: .utf8)! @@ -227,9 +197,9 @@ class BencodeSpec: QuickSpec { } } - // MARK: -- with a string value + // MARK: ---- with a string value context("with a string value") { - // MARK: ---- decodes successfully + // MARK: ------ decodes successfully it("decodes successfully") { let data: Data = "l4:Test5:\u{01}\u{02}\u{03}\u{04}\u{05}e".data(using: .utf8)! let result: BencodeResponse? = try? Bencode.decodeResponse(from: data) @@ -243,7 +213,7 @@ class BencodeSpec: QuickSpec { )) } - // MARK: ---- decodes successfully with no body + // MARK: ------ decodes successfully with no body it("decodes successfully with no body") { let data: Data = "l4:Teste".data(using: .utf8)! let result: BencodeResponse? = try? Bencode.decodeResponse(from: data) @@ -257,7 +227,7 @@ class BencodeSpec: QuickSpec { )) } - // MARK: ---- throws a parsing error when invalid + // MARK: ------ throws a parsing error when invalid it("throws a parsing error when invalid") { let data: Data = "l10:Teste".data(using: .utf8)! @@ -268,9 +238,9 @@ class BencodeSpec: QuickSpec { } } - // MARK: -- with an int value + // MARK: ---- with an int value context("with an int value") { - // MARK: ---- decodes successfully + // MARK: ------ decodes successfully it("decodes successfully") { let data: Data = "li100e5:\u{01}\u{02}\u{03}\u{04}\u{05}e".data(using: .utf8)! let result: BencodeResponse? = try? Bencode.decodeResponse(from: data) @@ -284,7 +254,7 @@ class BencodeSpec: QuickSpec { )) } - // MARK: ---- decodes successfully with no body + // MARK: ------ decodes successfully with no body it("decodes successfully with no body") { let data: Data = "li100ee".data(using: .utf8)! let result: BencodeResponse? = try? Bencode.decodeResponse(from: data) @@ -298,7 +268,7 @@ class BencodeSpec: QuickSpec { )) } - // MARK: ---- throws a parsing error when invalid + // MARK: ------ throws a parsing error when invalid it("throws a parsing error when invalid") { let data: Data = "l4:Teste".data(using: .utf8)! @@ -312,3 +282,34 @@ class BencodeSpec: QuickSpec { } } } + +// MARK: - Test Types + +fileprivate struct TestType: Codable, Equatable { + let intValue: Int + let stringValue: String +} + +fileprivate struct TestType2: Codable, Equatable { + let stringValue: String + let boolValue: Bool +} + +fileprivate struct TestType3: Codable, Equatable { + let stringValue: String + let boolValue: Bool + + init(_ stringValue: String, _ boolValue: Bool) { + self.stringValue = stringValue + self.boolValue = boolValue + } + + init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + self = TestType3( + try container.decode(String.self, forKey: .stringValue), + ((try? container.decode(Bool.self, forKey: .boolValue)) ?? false) + ) + } +} diff --git a/SessionUtilitiesKitTests/Utilities/VersionSpec.swift b/SessionUtilitiesKitTests/Utilities/VersionSpec.swift index 55c25ba4d..b3d05ad69 100644 --- a/SessionUtilitiesKitTests/Utilities/VersionSpec.swift +++ b/SessionUtilitiesKitTests/Utilities/VersionSpec.swift @@ -8,10 +8,10 @@ import Nimble @testable import SessionUtilitiesKit class VersionSpec: QuickSpec { - // MARK: - Spec - - override func spec() { + override class func spec() { + // MARK: - a Version describe("a Version") { + // MARK: -- can be created from a string it("can be created from a string") { let version: Version = Version.from("1.20.3") @@ -20,13 +20,16 @@ class VersionSpec: QuickSpec { expect(version.patch).to(equal(3)) } + // MARK: -- correctly exposes a string value it("correctly exposes a string value") { let version: Version = Version(major: 1, minor: 20, patch: 3) expect(version.stringValue).to(equal("1.20.3")) } + // MARK: -- when checking equality context("when checking equality") { + // MARK: ---- returns true if the values match it("returns true if the values match") { let version1: Version = Version.from("1.0.0") let version2: Version = Version.from("1.0.0") @@ -35,6 +38,7 @@ class VersionSpec: QuickSpec { .to(beTrue()) } + // MARK: ---- returns false if the values do not match it("returns false if the values do not match") { let version1: Version = Version.from("1.0.0") let version2: Version = Version.from("1.0.1") @@ -44,7 +48,9 @@ class VersionSpec: QuickSpec { } } + // MARK: -- when comparing versions context("when comparing versions") { + // MARK: ---- returns correctly for a simple major difference it("returns correctly for a simple major difference") { let version1: Version = Version.from("1.0.0") let version2: Version = Version.from("2.0.0") @@ -53,6 +59,7 @@ class VersionSpec: QuickSpec { expect(version2 > version1).to(beTrue()) } + // MARK: ---- returns correctly for a complex major difference it("returns correctly for a complex major difference") { let version1a: Version = Version.from("2.90.90") let version2a: Version = Version.from("10.0.0") @@ -65,6 +72,7 @@ class VersionSpec: QuickSpec { expect(version2b > version1b).to(beTrue()) } + // MARK: ---- returns correctly for a simple minor difference it("returns correctly for a simple minor difference") { let version1: Version = Version.from("1.0.0") let version2: Version = Version.from("1.1.0") @@ -73,6 +81,7 @@ class VersionSpec: QuickSpec { expect(version2 > version1).to(beTrue()) } + // MARK: ---- returns correctly for a complex minor difference it("returns correctly for a complex minor difference") { let version1a: Version = Version.from("90.2.90") let version2a: Version = Version.from("90.10.0") @@ -85,6 +94,7 @@ class VersionSpec: QuickSpec { expect(version2b > version1b).to(beTrue()) } + // MARK: ---- returns correctly for a simple patch difference it("returns correctly for a simple patch difference") { let version1: Version = Version.from("1.0.0") let version2: Version = Version.from("1.0.1") @@ -93,6 +103,7 @@ class VersionSpec: QuickSpec { expect(version2 > version1).to(beTrue()) } + // MARK: ---- returns correctly for a complex patch difference it("returns correctly for a complex patch difference") { let version1a: Version = Version.from("90.90.2") let version2a: Version = Version.from("90.90.10") diff --git a/_SharedTestUtilities/Mock.swift b/_SharedTestUtilities/Mock.swift index aed05ca40..042de35ad 100644 --- a/_SharedTestUtilities/Mock.swift +++ b/_SharedTestUtilities/Mock.swift @@ -17,9 +17,13 @@ public class Mock { // MARK: - Initialization - internal required init(functionHandler: MockFunctionHandler? = nil) { + internal required init( + functionHandler: MockFunctionHandler? = nil, + initialSetup: ((Mock) -> ())? = nil + ) { self.functionConsumer = FunctionConsumer() self.functionHandler = (functionHandler ?? self.functionConsumer) + initialSetup?(self) } // MARK: - MockFunctionHandler @@ -103,7 +107,7 @@ internal class MockFunction { internal class MockFunctionBuilder: MockFunctionHandler { private let callBlock: (inout T) throws -> R - private let mockInit: (MockFunctionHandler?) -> Mock + private let mockInit: (MockFunctionHandler?, ((Mock) -> ())?) -> Mock private var functionName: String? private var parameterCount: Int? private var parameterSummary: String? @@ -113,7 +117,7 @@ internal class MockFunctionBuilder: MockFunctionHandler { // MARK: - Initialization - init(_ callBlock: @escaping (inout T) throws -> R, mockInit: @escaping (MockFunctionHandler?) -> Mock) { + init(_ callBlock: @escaping (inout T) throws -> R, mockInit: @escaping (MockFunctionHandler?, ((Mock) -> ())?) -> Mock) { self.callBlock = callBlock self.mockInit = mockInit } @@ -141,7 +145,7 @@ internal class MockFunctionBuilder: MockFunctionHandler { // MARK: - Build func build() throws -> MockFunction { - var completionMock = mockInit(self) as! T + var completionMock = mockInit(self, nil) as! T _ = try? callBlock(&completionMock) guard let name: String = functionName, let parameterCount: Int = parameterCount, let parameterSummary: String = parameterSummary else { diff --git a/_SharedTestUtilities/MockCaches.swift b/_SharedTestUtilities/MockCaches.swift index 493f2c579..94125118a 100644 --- a/_SharedTestUtilities/MockCaches.swift +++ b/_SharedTestUtilities/MockCaches.swift @@ -17,6 +17,11 @@ class MockCaches: CachesType { set { cacheInstances[cache.key] = newValue.map { cache.mutableInstance($0) } } } + public func setting(cache: CacheInfo.Config, to value: M?) -> MockCaches { + self[cache] = value + return self + } + // MARK: - Mutable Access @discardableResult public func mutate( diff --git a/SessionMessagingKitTests/_TestUtilities/MockUserDefaults.swift b/_SharedTestUtilities/MockUserDefaults.swift similarity index 100% rename from SessionMessagingKitTests/_TestUtilities/MockUserDefaults.swift rename to _SharedTestUtilities/MockUserDefaults.swift diff --git a/_SharedTestUtilities/Mocked.swift b/_SharedTestUtilities/Mocked.swift index e5e9e9cd2..323c47cc9 100644 --- a/_SharedTestUtilities/Mocked.swift +++ b/_SharedTestUtilities/Mocked.swift @@ -1,6 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation +import GRDB import SessionUtilitiesKit // MARK: - Mocked @@ -32,6 +33,20 @@ func any() -> Double { 0 } func any() -> String { "" } func any() -> Data { Data() } func any() -> Bool { false } +func any() -> Dependencies { + Dependencies( + storage: SynchronousStorage(customWriter: try! DatabaseQueue()), + network: MockNetwork(), + crypto: MockCrypto(), + standardUserDefaults: MockUserDefaults(), + caches: MockCaches(), + jobRunner: MockJobRunner(), + scheduler: .immediate, + dateNow: Date(timeIntervalSince1970: 1234567890), + fixedTime: 0, + forceSynchronous: true + ) +} func anyAny() -> Any { 0 } // Unique name for compilation performance reasons func anyArray() -> [R] { [] } // Unique name for compilation performance reasons diff --git a/_SharedTestUtilities/NimbleExtensions.swift b/_SharedTestUtilities/NimbleExtensions.swift index 12b5d740d..cfb64752b 100644 --- a/_SharedTestUtilities/NimbleExtensions.swift +++ b/_SharedTestUtilities/NimbleExtensions.swift @@ -29,7 +29,7 @@ public func call( matchingParameters: Bool = false, exclusive: Bool = false, functionBlock: @escaping (inout T) throws -> R -) -> Predicate where M: Mock { +) -> Nimble.Predicate where M: Mock { return Predicate.define { actualExpression in let callInfo: CallInfo = generateCallInfo(actualExpression, functionBlock) let matchingParameterRecords: [String] = callInfo.desiredFunctionCalls diff --git a/_SharedTestUtilities/SynchronousStorage.swift b/_SharedTestUtilities/SynchronousStorage.swift index 86471f988..66680cd2e 100644 --- a/_SharedTestUtilities/SynchronousStorage.swift +++ b/_SharedTestUtilities/SynchronousStorage.swift @@ -6,6 +6,16 @@ import GRDB @testable import SessionUtilitiesKit class SynchronousStorage: Storage { + public init( + customWriter: DatabaseWriter? = nil, + customMigrationTargets: [MigratableTarget.Type]? = nil, + initialData: ((Database) throws -> ())? = nil + ) { + super.init(customWriter: customWriter, customMigrationTargets: customMigrationTargets) + + write { db in try initialData?(db) } + } + @discardableResult override func write( fileName: String = #file, functionName: String = #function,