From 530910bba3b68d75ca23038b09583315aeffacfc Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 21 Nov 2023 09:17:50 +0000 Subject: [PATCH] Get rid of the dependency between TestTools and TestHelpers (#2897) --- .swiftlint.yml | 2 + StreamChat.xcodeproj/project.pbxproj | 259 +++-- .../Extensions/Dictionary.swift | 2 +- .../Extensions/String.swift | 2 +- .../{HttpServer.swift => Swifter.swift} | 1 - .../MockServer/AttachmentResponses.swift | 1 - .../MockServer/ChannelConfig.swift | 1 - .../MockServer/ChannelResponses.swift | 1 - .../MockServer/EventResponses.swift | 1 - .../MockServer/MembersResponse.swift | 1 - .../MockServer/MessageResponses.swift | 1 - .../MockServer/ReactionResponses.swift | 1 - .../MockServer/StreamMockServer.swift | 1 - .../MockServer/User.swift | 1 - .../MockServer/WebsocketResponses.swift | 1 - .../Robots/ParticipantRobot.swift | 1 - .../Swifter/DemoServer.swift | 205 ++++ .../Swifter/Errno.swift | 16 + .../Swifter/Files.swift | 106 +++ .../Swifter/HttpParser.swift | 64 ++ .../Swifter/HttpRequest.swift | 173 ++++ .../Swifter/HttpResponse.swift | 190 ++++ .../Swifter/HttpRouter.swift | 192 ++++ .../Swifter/HttpServer.swift | 84 ++ .../Swifter/HttpServerIO.swift | 204 ++++ .../Swifter/MimeTypes.swift | 141 +++ .../Swifter/Process.swift | 40 + .../Swifter/Scopes.swift | 887 ++++++++++++++++++ .../Swifter/Socket+File.swift | 57 ++ .../Swifter/Socket+Server.swift | 116 +++ .../Swifter/Socket.swift | 236 +++++ .../Swifter/String+BASE64.swift | 15 + .../Swifter/String+File.swift | 147 +++ .../Swifter/String+Misc.swift | 34 + .../Swifter/String+SHA1.swift | 137 +++ .../Swifter/WebSockets.swift | 295 ++++++ .../Utilities/TestData.swift | 1 - .../Assertions/AssertAsync.swift | 566 +++++++++++ .../Assertions/AssertDate.swift | 28 + .../Assertions/AssertJSONEqual.swift | 173 ++++ .../AssertNetworkRequest.swift | 0 .../Assertions/AssertResult.swift | 62 ++ .../Assertions/AssertTestQueue.swift | 32 + .../Assertions/UnwrapAsync.swift | 32 + .../Difference/Difference.swift | 460 +++++++++ .../Extensions/XCTest+Helpers.swift | 335 +++++++ .../Extensions/XCTestCase+MockData.swift | 8 +- .../Extensions/XCTestCase+StressTest.swift | 25 + .../XCTestCase+TestImages.swift | 0 .../Extensions/XCTestCase+iOS13.swift | 22 + .../ChatChannelListController_Mock.swift | 1 - .../Mocks/StreamChat/MockFunc.swift | 61 ++ .../AuthenticationRepository_Mock.swift | 1 - .../QueueAware/QueueAwareDelegate.swift | 37 + .../SpyPattern/Spy/Spy.swift | 60 ++ .../StreamChatTestTools.swift | 10 +- .../TestRunnerEnvironment.swift | 21 + .../StreamChatTestTools/Wait/WaitFor.swift | 59 ++ .../StreamChatTestTools/Wait/WaitUntil.swift | 22 + .../Payloads/CreateCallPayload_Tests.swift | 2 +- .../Payloads/MessagePayloads_Tests.swift | 5 +- .../Payloads/UserPayloads_Tests.swift | 5 +- .../Requests/CallRequestBody_Tests.swift | 2 +- .../Audio/StreamAppStateObserver_Tests.swift | 1 + .../Database/DTOs/AttachmentDTO_Tests.swift | 5 +- .../AnyAttachmentUpdater_Tests.swift | 1 + .../GiphyAttachmentPayload_Tests.swift | 1 + .../Query/MessageSearchQuery_Tests.swift | 1 + .../Atomic_StressTests.swift | 1 + .../Cached_StressTests.swift | 1 + Tests/StreamChatTests/StreamChatTests.swift | 2 - .../Utils/CooldownTracker_Tests.swift | 1 - .../Utils/Debouncer_Tests.swift | 1 - .../Utils/MulticastDelegate_Tests.swift | 2 +- .../Operations/AsyncOperation_Tests.swift | 1 + ...dioQueuePlayerNextItemProvider_Tests.swift | 2 +- ...irectionalPanGestureRecogniser_Tests.swift | 2 +- 77 files changed, 5563 insertions(+), 105 deletions(-) rename TestTools/StreamChatTestMockServer/Extensions/{HttpServer.swift => Swifter.swift} (97%) create mode 100644 TestTools/StreamChatTestMockServer/Swifter/DemoServer.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/Errno.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/Files.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/HttpParser.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/HttpRequest.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/HttpResponse.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/HttpRouter.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/HttpServer.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/HttpServerIO.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/MimeTypes.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/Process.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/Scopes.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/Socket+File.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/Socket+Server.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/Socket.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/String+BASE64.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/String+File.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/String+Misc.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/String+SHA1.swift create mode 100644 TestTools/StreamChatTestMockServer/Swifter/WebSockets.swift create mode 100644 TestTools/StreamChatTestTools/Assertions/AssertAsync.swift create mode 100644 TestTools/StreamChatTestTools/Assertions/AssertDate.swift create mode 100644 TestTools/StreamChatTestTools/Assertions/AssertJSONEqual.swift rename TestTools/StreamChatTestTools/{ => Assertions}/AssertNetworkRequest.swift (100%) create mode 100644 TestTools/StreamChatTestTools/Assertions/AssertResult.swift create mode 100644 TestTools/StreamChatTestTools/Assertions/AssertTestQueue.swift create mode 100644 TestTools/StreamChatTestTools/Assertions/UnwrapAsync.swift create mode 100644 TestTools/StreamChatTestTools/Difference/Difference.swift create mode 100644 TestTools/StreamChatTestTools/Extensions/XCTest+Helpers.swift create mode 100644 TestTools/StreamChatTestTools/Extensions/XCTestCase+StressTest.swift rename TestTools/StreamChatTestTools/{ => Extensions}/XCTestCase+TestImages.swift (100%) create mode 100644 TestTools/StreamChatTestTools/Extensions/XCTestCase+iOS13.swift create mode 100644 TestTools/StreamChatTestTools/Mocks/StreamChat/MockFunc.swift create mode 100644 TestTools/StreamChatTestTools/SpyPattern/QueueAware/QueueAwareDelegate.swift create mode 100644 TestTools/StreamChatTestTools/SpyPattern/Spy/Spy.swift create mode 100644 TestTools/StreamChatTestTools/TestRunnerEnvironment.swift create mode 100644 TestTools/StreamChatTestTools/Wait/WaitFor.swift create mode 100644 TestTools/StreamChatTestTools/Wait/WaitUntil.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 321666a10c6..18b317d846e 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -16,6 +16,8 @@ excluded: - Pods - .build - spm_cache + - vendor/bundle + - .ruby-lsp disabled_rules: - large_tuple diff --git a/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj index b2637e7b589..9860cbbfe9b 100644 --- a/StreamChat.xcodeproj/project.pbxproj +++ b/StreamChat.xcodeproj/project.pbxproj @@ -493,20 +493,35 @@ 825A32CB27DBB463000402A9 /* MessageListPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 825A32CA27DBB463000402A9 /* MessageListPage.swift */; }; 825A32CD27DBB46F000402A9 /* ChannelListPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 825A32CC27DBB46F000402A9 /* ChannelListPage.swift */; }; 825A32CF27DBB48D000402A9 /* StartPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 825A32CE27DBB48D000402A9 /* StartPage.swift */; }; + 8263464C2B0BACF600122D0E /* Difference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8263464B2B0BACF600122D0E /* Difference.swift */; }; + 826346622B0BAE3800122D0E /* Socket+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8263464E2B0BAE3600122D0E /* Socket+File.swift */; }; + 826346632B0BAE3800122D0E /* String+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8263464F2B0BAE3600122D0E /* String+File.swift */; }; + 826346642B0BAE3800122D0E /* HttpParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826346502B0BAE3600122D0E /* HttpParser.swift */; }; + 826346652B0BAE3800122D0E /* Socket+Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826346512B0BAE3600122D0E /* Socket+Server.swift */; }; + 826346662B0BAE3800122D0E /* String+Misc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826346522B0BAE3600122D0E /* String+Misc.swift */; }; + 826346672B0BAE3800122D0E /* Scopes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826346532B0BAE3600122D0E /* Scopes.swift */; }; + 826346682B0BAE3800122D0E /* String+SHA1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826346542B0BAE3600122D0E /* String+SHA1.swift */; }; + 826346692B0BAE3800122D0E /* String+BASE64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826346552B0BAE3600122D0E /* String+BASE64.swift */; }; + 8263466A2B0BAE3800122D0E /* MimeTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826346562B0BAE3700122D0E /* MimeTypes.swift */; }; + 8263466B2B0BAE3800122D0E /* Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826346572B0BAE3700122D0E /* Files.swift */; }; + 8263466C2B0BAE3800122D0E /* WebSockets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826346582B0BAE3700122D0E /* WebSockets.swift */; }; + 8263466D2B0BAE3800122D0E /* HttpRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826346592B0BAE3700122D0E /* HttpRouter.swift */; }; + 8263466E2B0BAE3800122D0E /* DemoServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8263465A2B0BAE3700122D0E /* DemoServer.swift */; }; + 8263466F2B0BAE3800122D0E /* HttpServerIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8263465B2B0BAE3700122D0E /* HttpServerIO.swift */; }; + 826346702B0BAE3800122D0E /* HttpResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8263465C2B0BAE3700122D0E /* HttpResponse.swift */; }; + 826346712B0BAE3800122D0E /* HttpRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8263465D2B0BAE3700122D0E /* HttpRequest.swift */; }; + 826346722B0BAE3800122D0E /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8263465E2B0BAE3700122D0E /* Process.swift */; }; + 826346732B0BAE3800122D0E /* HttpServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8263465F2B0BAE3800122D0E /* HttpServer.swift */; }; + 826346742B0BAE3800122D0E /* Errno.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826346602B0BAE3800122D0E /* Errno.swift */; }; + 826346752B0BAE3800122D0E /* Socket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826346612B0BAE3800122D0E /* Socket.swift */; }; 826992C82900628500D2D470 /* DeviceRemoteControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826992C72900628500D2D470 /* DeviceRemoteControl.swift */; }; 826B1C3528895AFD005DDF13 /* http_youtube_link.json in Resources */ = {isa = PBXBuildFile; fileRef = 826B1C3428895AFD005DDF13 /* http_youtube_link.json */; }; 826B1C3728895BB5005DDF13 /* http_unsplash_link.json in Resources */ = {isa = PBXBuildFile; fileRef = 826B1C3628895BB5005DDF13 /* http_unsplash_link.json */; }; 826B1C39288FD756005DDF13 /* http_truncate.json in Resources */ = {isa = PBXBuildFile; fileRef = 826B1C38288FD756005DDF13 /* http_truncate.json */; }; 826EF2B1291C01C1005A9EEF /* Authentication_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826EF2B0291C01C1005A9EEF /* Authentication_Tests.swift */; }; 827414272ACDE941009CD13C /* StreamChatTestMockServer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A3A0C999283E952900B18DA4 /* StreamChatTestMockServer.framework */; platformFilter = ios; }; - 8274142D2ACDEBAD009CD13C /* Swifter in Frameworks */ = {isa = PBXBuildFile; productRef = 8274142C2ACDEBAD009CD13C /* Swifter */; }; 827414412ACDF6C2009CD13C /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827414402ACDF6C2009CD13C /* String.swift */; }; 827414432ACDF76C009CD13C /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827414422ACDF76C009CD13C /* Dictionary.swift */; }; - 827418152ACDE820004A23DA /* StreamSwiftTestHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 827418142ACDE820004A23DA /* StreamSwiftTestHelpers */; settings = {ATTRIBUTES = (Required, ); }; }; - 827418172ACDE830004A23DA /* StreamSwiftTestHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 827418162ACDE830004A23DA /* StreamSwiftTestHelpers */; }; - 827418192ACDE83A004A23DA /* StreamSwiftTestHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 827418182ACDE83A004A23DA /* StreamSwiftTestHelpers */; }; - 8274181B2ACDE844004A23DA /* StreamSwiftTestHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 8274181A2ACDE844004A23DA /* StreamSwiftTestHelpers */; }; - 8274181D2ACDE851004A23DA /* StreamSwiftTestHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 8274181C2ACDE851004A23DA /* StreamSwiftTestHelpers */; }; 8274181F2ACDE85E004A23DA /* StreamSwiftTestHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 8274181E2ACDE85E004A23DA /* StreamSwiftTestHelpers */; }; 827418212ACDE86F004A23DA /* StreamSwiftTestHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 827418202ACDE86F004A23DA /* StreamSwiftTestHelpers */; }; 8279706F29689680006741A3 /* UserDetails_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8279706E29689680006741A3 /* UserDetails_Tests.swift */; }; @@ -525,6 +540,23 @@ 82DCB3AD2A4AE8FB00738933 /* StreamChatUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 790881FD25432B7200896F03 /* StreamChatUI.framework */; }; 82DCB3AE2A4AE8FB00738933 /* StreamChatUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 790881FD25432B7200896F03 /* StreamChatUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 82E43AC128637651007BB6CD /* http_attachment.json in Resources */ = {isa = PBXBuildFile; fileRef = 82E43AC028637651007BB6CD /* http_attachment.json */; }; + 82E655332B06748400D64906 /* Spy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E655322B06748400D64906 /* Spy.swift */; }; + 82E655352B06751D00D64906 /* QueueAwareDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E655342B06751D00D64906 /* QueueAwareDelegate.swift */; }; + 82E655372B06756A00D64906 /* AssertTestQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E655362B06756A00D64906 /* AssertTestQueue.swift */; }; + 82E655392B06775D00D64906 /* MockFunc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E655382B06775D00D64906 /* MockFunc.swift */; }; + 82E6553B2B0677EA00D64906 /* TestRunnerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E6553A2B0677EA00D64906 /* TestRunnerEnvironment.swift */; }; + 82E6553C2B06785700D64906 /* StreamChatTestTools.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 793060E625778896005CF846 /* StreamChatTestTools.framework */; }; + 82E6553F2B06798100D64906 /* AssertJSONEqual.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E6553E2B06798100D64906 /* AssertJSONEqual.swift */; }; + 82E655412B067A4C00D64906 /* WaitFor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E655402B067A4C00D64906 /* WaitFor.swift */; }; + 82E655432B067C3600D64906 /* AssertAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E655422B067C3600D64906 /* AssertAsync.swift */; }; + 82E655452B067CAE00D64906 /* AssertResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E655442B067CAE00D64906 /* AssertResult.swift */; }; + 82E6554B2B067ED700D64906 /* WaitUntil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E6554A2B067ED700D64906 /* WaitUntil.swift */; }; + 82F714A12B077F3300442A74 /* XCTestCase+iOS13.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82F714A02B077F3300442A74 /* XCTestCase+iOS13.swift */; }; + 82F714A32B077FDE00442A74 /* XCTestCase+StressTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82F714A22B077FDE00442A74 /* XCTestCase+StressTest.swift */; }; + 82F714A52B07831700442A74 /* AssertDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82F714A42B07831700442A74 /* AssertDate.swift */; }; + 82F714A72B0784D900442A74 /* UnwrapAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82F714A62B0784D900442A74 /* UnwrapAsync.swift */; }; + 82F714A92B0785D900442A74 /* XCTest+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82F714A82B0785D900442A74 /* XCTest+Helpers.swift */; }; + 82F714AB2B078AE800442A74 /* StreamSwiftTestHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 82F714AA2B078AE800442A74 /* StreamSwiftTestHelpers */; }; 840B4FCF26A9E53100D5EFAB /* CustomEventRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840B4FCE26A9E53100D5EFAB /* CustomEventRequestBody.swift */; }; 84196FA32805892500185E99 /* LocalMessageState+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84196FA22805892500185E99 /* LocalMessageState+Extensions.swift */; }; 842F9745277A09B10060A489 /* PinnedMessagesQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842F9744277A09B10060A489 /* PinnedMessagesQuery.swift */; }; @@ -960,7 +992,7 @@ A39A8AE7263825F4003453D9 /* ChatMessageLayoutOptionsResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A39A8AE6263825F4003453D9 /* ChatMessageLayoutOptionsResolver.swift */; }; A39B040B27F196F200D6B18A /* StreamChatUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A39B040A27F196F200D6B18A /* StreamChatUITests.swift */; }; A3A0C9A1283E955200B18DA4 /* ChannelResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 824445AA27EA364300DB2FD8 /* ChannelResponses.swift */; }; - A3A0C9A3283E955200B18DA4 /* HttpServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8224FD85280EC09800B32D43 /* HttpServer.swift */; }; + A3A0C9A3283E955200B18DA4 /* Swifter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8224FD85280EC09800B32D43 /* Swifter.swift */; }; A3A0C9A5283E955200B18DA4 /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 825A32AD27DA686C000402A9 /* TestData.swift */; }; A3A0C9A6283E955200B18DA4 /* WebsocketResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E4CBDB27F240C60013B02D /* WebsocketResponses.swift */; }; A3A0C9A9283E955200B18DA4 /* MessageResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B350FC27EA2A3900FEB6A0 /* MessageResponses.swift */; }; @@ -3090,7 +3122,7 @@ 8223EE7B2937A0E9006138B9 /* http_channel_creation.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = http_channel_creation.json; sourceTree = ""; }; 8223EE7D2937A1F5006138B9 /* http_channel_removal.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = http_channel_removal.json; sourceTree = ""; }; 8223EE7F2937A21E006138B9 /* ws_message.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ws_message.json; sourceTree = ""; }; - 8224FD85280EC09800B32D43 /* HttpServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpServer.swift; sourceTree = ""; }; + 8224FD85280EC09800B32D43 /* Swifter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Swifter.swift; sourceTree = ""; }; 8232B84E28635C4A0032C7DB /* Attachments_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachments_Tests.swift; sourceTree = ""; }; 82390E7A28C609A700829581 /* push_notification.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = push_notification.json; sourceTree = ""; }; 823A1AD928C74C1400F7CADA /* SpringBoard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpringBoard.swift; sourceTree = ""; }; @@ -3102,6 +3134,27 @@ 825A32CC27DBB46F000402A9 /* ChannelListPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelListPage.swift; sourceTree = ""; }; 825A32CE27DBB48D000402A9 /* StartPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartPage.swift; sourceTree = ""; }; 8261340927F20B7A0034AC37 /* StreamChatUITestsApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = StreamChatUITestsApp.xctestplan; sourceTree = ""; }; + 8263464B2B0BACF600122D0E /* Difference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Difference.swift; sourceTree = ""; }; + 8263464E2B0BAE3600122D0E /* Socket+File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Socket+File.swift"; sourceTree = ""; }; + 8263464F2B0BAE3600122D0E /* String+File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+File.swift"; sourceTree = ""; }; + 826346502B0BAE3600122D0E /* HttpParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpParser.swift; sourceTree = ""; }; + 826346512B0BAE3600122D0E /* Socket+Server.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Socket+Server.swift"; sourceTree = ""; }; + 826346522B0BAE3600122D0E /* String+Misc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Misc.swift"; sourceTree = ""; }; + 826346532B0BAE3600122D0E /* Scopes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scopes.swift; sourceTree = ""; }; + 826346542B0BAE3600122D0E /* String+SHA1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SHA1.swift"; sourceTree = ""; }; + 826346552B0BAE3600122D0E /* String+BASE64.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+BASE64.swift"; sourceTree = ""; }; + 826346562B0BAE3700122D0E /* MimeTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MimeTypes.swift; sourceTree = ""; }; + 826346572B0BAE3700122D0E /* Files.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Files.swift; sourceTree = ""; }; + 826346582B0BAE3700122D0E /* WebSockets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSockets.swift; sourceTree = ""; }; + 826346592B0BAE3700122D0E /* HttpRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpRouter.swift; sourceTree = ""; }; + 8263465A2B0BAE3700122D0E /* DemoServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoServer.swift; sourceTree = ""; }; + 8263465B2B0BAE3700122D0E /* HttpServerIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpServerIO.swift; sourceTree = ""; }; + 8263465C2B0BAE3700122D0E /* HttpResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpResponse.swift; sourceTree = ""; }; + 8263465D2B0BAE3700122D0E /* HttpRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpRequest.swift; sourceTree = ""; }; + 8263465E2B0BAE3700122D0E /* Process.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Process.swift; sourceTree = ""; }; + 8263465F2B0BAE3800122D0E /* HttpServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpServer.swift; sourceTree = ""; }; + 826346602B0BAE3800122D0E /* Errno.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errno.swift; sourceTree = ""; }; + 826346612B0BAE3800122D0E /* Socket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Socket.swift; sourceTree = ""; }; 826992C72900628500D2D470 /* DeviceRemoteControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRemoteControl.swift; sourceTree = ""; }; 826B1C3428895AFD005DDF13 /* http_youtube_link.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = http_youtube_link.json; sourceTree = ""; }; 826B1C3628895BB5005DDF13 /* http_unsplash_link.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = http_unsplash_link.json; sourceTree = ""; }; @@ -3133,6 +3186,21 @@ 82E4CBDB27F240C60013B02D /* WebsocketResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsocketResponses.swift; sourceTree = ""; }; 82E4CBDD27F30E7D0013B02D /* MessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageList.swift; sourceTree = ""; }; 82E4CBDF27F322EA0013B02D /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 82E655322B06748400D64906 /* Spy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spy.swift; sourceTree = ""; }; + 82E655342B06751D00D64906 /* QueueAwareDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueAwareDelegate.swift; sourceTree = ""; }; + 82E655362B06756A00D64906 /* AssertTestQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertTestQueue.swift; sourceTree = ""; }; + 82E655382B06775D00D64906 /* MockFunc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFunc.swift; sourceTree = ""; }; + 82E6553A2B0677EA00D64906 /* TestRunnerEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestRunnerEnvironment.swift; sourceTree = ""; }; + 82E6553E2B06798100D64906 /* AssertJSONEqual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertJSONEqual.swift; sourceTree = ""; }; + 82E655402B067A4C00D64906 /* WaitFor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitFor.swift; sourceTree = ""; }; + 82E655422B067C3600D64906 /* AssertAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertAsync.swift; sourceTree = ""; }; + 82E655442B067CAE00D64906 /* AssertResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertResult.swift; sourceTree = ""; }; + 82E6554A2B067ED700D64906 /* WaitUntil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitUntil.swift; sourceTree = ""; }; + 82F714A02B077F3300442A74 /* XCTestCase+iOS13.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+iOS13.swift"; sourceTree = ""; }; + 82F714A22B077FDE00442A74 /* XCTestCase+StressTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+StressTest.swift"; sourceTree = ""; }; + 82F714A42B07831700442A74 /* AssertDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertDate.swift; sourceTree = ""; }; + 82F714A62B0784D900442A74 /* UnwrapAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnwrapAsync.swift; sourceTree = ""; }; + 82F714A82B0785D900442A74 /* XCTest+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Helpers.swift"; sourceTree = ""; }; 840B4FCE26A9E53100D5EFAB /* CustomEventRequestBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEventRequestBody.swift; sourceTree = ""; }; 8414A81B2767944B001BA9D7 /* RetryStrategy_Spy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryStrategy_Spy.swift; sourceTree = ""; }; 84196FA22805892500185E99 /* LocalMessageState+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LocalMessageState+Extensions.swift"; sourceTree = ""; }; @@ -4238,8 +4306,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 82E6553C2B06785700D64906 /* StreamChatTestTools.framework in Frameworks */, 7908820625432B7200896F03 /* StreamChatUI.framework in Frameworks */, - 8274181D2ACDE851004A23DA /* StreamSwiftTestHelpers in Frameworks */, + 82F714AB2B078AE800442A74 /* StreamSwiftTestHelpers in Frameworks */, A3BD4815281A941B0090D511 /* AutomaticSettings in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4261,7 +4330,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 827418192ACDE83A004A23DA /* StreamSwiftTestHelpers in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4269,7 +4337,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 827418172ACDE830004A23DA /* StreamSwiftTestHelpers in Frameworks */, 793060EF25778897005CF846 /* StreamChatTestTools.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4286,7 +4353,6 @@ buildActionMask = 2147483647; files = ( F8788F81261DE9B0006019DD /* StreamChatTestTools.framework in Frameworks */, - 8274181B2ACDE844004A23DA /* StreamSwiftTestHelpers in Frameworks */, 799C9456247D59B1001F1104 /* StreamChat.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4323,8 +4389,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 827418152ACDE820004A23DA /* StreamSwiftTestHelpers in Frameworks */, - 8274142D2ACDEBAD009CD13C /* Swifter in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5302,6 +5366,41 @@ path = Pages; sourceTree = ""; }; + 8263464A2B0BACC600122D0E /* Difference */ = { + isa = PBXGroup; + children = ( + 8263464B2B0BACF600122D0E /* Difference.swift */, + ); + path = Difference; + sourceTree = ""; + }; + 8263464D2B0BADFF00122D0E /* Swifter */ = { + isa = PBXGroup; + children = ( + 8263465A2B0BAE3700122D0E /* DemoServer.swift */, + 826346602B0BAE3800122D0E /* Errno.swift */, + 826346572B0BAE3700122D0E /* Files.swift */, + 826346502B0BAE3600122D0E /* HttpParser.swift */, + 8263465D2B0BAE3700122D0E /* HttpRequest.swift */, + 8263465C2B0BAE3700122D0E /* HttpResponse.swift */, + 826346592B0BAE3700122D0E /* HttpRouter.swift */, + 8263465F2B0BAE3800122D0E /* HttpServer.swift */, + 8263465B2B0BAE3700122D0E /* HttpServerIO.swift */, + 826346562B0BAE3700122D0E /* MimeTypes.swift */, + 8263465E2B0BAE3700122D0E /* Process.swift */, + 826346532B0BAE3600122D0E /* Scopes.swift */, + 826346612B0BAE3800122D0E /* Socket.swift */, + 8263464E2B0BAE3600122D0E /* Socket+File.swift */, + 826346512B0BAE3600122D0E /* Socket+Server.swift */, + 826346552B0BAE3600122D0E /* String+BASE64.swift */, + 8263464F2B0BAE3600122D0E /* String+File.swift */, + 826346522B0BAE3600122D0E /* String+Misc.swift */, + 826346542B0BAE3600122D0E /* String+SHA1.swift */, + 826346582B0BAE3700122D0E /* WebSockets.swift */, + ); + path = Swifter; + sourceTree = ""; + }; 829CD5C22848C244003C3877 /* Robots */ = { isa = PBXGroup; children = ( @@ -5377,6 +5476,29 @@ path = Tests; sourceTree = ""; }; + 82E6553D2B06796F00D64906 /* Assertions */ = { + isa = PBXGroup; + children = ( + A3F65E3227EB6F63003F6256 /* AssertNetworkRequest.swift */, + 82E655362B06756A00D64906 /* AssertTestQueue.swift */, + 82E6553E2B06798100D64906 /* AssertJSONEqual.swift */, + 82E655422B067C3600D64906 /* AssertAsync.swift */, + 82E655442B067CAE00D64906 /* AssertResult.swift */, + 82F714A42B07831700442A74 /* AssertDate.swift */, + 82F714A62B0784D900442A74 /* UnwrapAsync.swift */, + ); + path = Assertions; + sourceTree = ""; + }; + 82E655492B067EC700D64906 /* Wait */ = { + isa = PBXGroup; + children = ( + 82E655402B067A4C00D64906 /* WaitFor.swift */, + 82E6554A2B067ED700D64906 /* WaitUntil.swift */, + ); + path = Wait; + sourceTree = ""; + }; 842F9747277A1CBE0060A489 /* PinnedMessages */ = { isa = PBXGroup; children = ( @@ -5957,9 +6079,10 @@ children = ( A344077127D753530044F150 /* StreamChatTestTools.h */, A344077227D753530044F150 /* Info.plist */, - A3F65E3227EB6F63003F6256 /* AssertNetworkRequest.swift */, + 82E6553A2B0677EA00D64906 /* TestRunnerEnvironment.swift */, A311B42C27E8BB7400CFCF6D /* StreamChatTestTools.swift */, - 792E3DDE25CACFA80040B0C2 /* XCTestCase+TestImages.swift */, + 82E655492B067EC700D64906 /* Wait */, + 82E6553D2B06796F00D64906 /* Assertions */, A311B43D27E8BCA300CFCF6D /* DatabaseModels */, A3C3BC7627E8AA7400224761 /* Extensions */, A364D08727CFBA3F0029857A /* Mocks */, @@ -5967,6 +6090,7 @@ A3C3BC8127E8AB1E00224761 /* SpyPattern */, A3C7BA7A27E3785500BBF4FA /* TestData */, A3D15D8227E9D4B5006B34D7 /* VirtualTime */, + 8263464A2B0BACC600122D0E /* Difference */, ); path = StreamChatTestTools; sourceTree = ""; @@ -6191,6 +6315,8 @@ children = ( 40D483F22A1264F1009E4134 /* VoiceRecording */, 7991D83E24F8F1BF00D21BA3 /* ChatClient_Mock.swift */, + 82E655382B06775D00D64906 /* MockFunc.swift */, + A364D09727D0C5940029857A /* Workers */, A364D0A627D128220029857A /* Controllers */, A364D09C27D0C6BE0029857A /* Database */, A364D0B127D1298A0029857A /* MemberListController */, @@ -6198,7 +6324,6 @@ C12D0A5E28FD58CE0099895A /* Repositories */, A364D0BC27D12C260029857A /* Utils */, A364D09E27D0C74E0029857A /* WebSocketClient */, - A364D09727D0C5940029857A /* Workers */, ); path = StreamChat; sourceTree = ""; @@ -7110,6 +7235,7 @@ 829CD5C22848C244003C3877 /* Robots */, A3C049C62840BDA700E25E38 /* Fixtures */, 82AD02B527D8E3C7000611B7 /* Utilities */, + 8263464D2B0BADFF00122D0E /* Swifter */, ); path = StreamChatTestMockServer; sourceTree = ""; @@ -7117,7 +7243,7 @@ A3A2D3432837D99E00D45F6A /* Extensions */ = { isa = PBXGroup; children = ( - 8224FD85280EC09800B32D43 /* HttpServer.swift */, + 8224FD85280EC09800B32D43 /* Swifter.swift */, 827414402ACDF6C2009CD13C /* String.swift */, 827414422ACDF76C009CD13C /* Dictionary.swift */, ); @@ -7156,6 +7282,7 @@ A3C7BABA27E4D97500BBF4FA /* UserController_Delegate.swift */, A3C7BACC27E4DD9000BBF4FA /* UserListController_Delegate.swift */, A3C7BADC27E4E86400BBF4FA /* WebSocketPingController_Delegate.swift */, + 82E655342B06751D00D64906 /* QueueAwareDelegate.swift */, ); path = QueueAware; sourceTree = ""; @@ -7206,6 +7333,7 @@ A3C3BC7627E8AA7400224761 /* Extensions */ = { isa = PBXGroup; children = ( + A3C7BAA727E38C4A00BBF4FA /* Unique */, A3C7BA8B27E37E7700BBF4FA /* AnyEncodable+Equatable.swift */, A3C7BA9127E37FA200BBF4FA /* Array+Subscript.swift */, A3C7BAD627E4E51600BBF4FA /* Calendar+GMT.swift */, @@ -7219,9 +7347,12 @@ A3C7BA9D27E3893A00BBF4FA /* String+Date.swift */, A3C7BA8F27E37EE000BBF4FA /* URLSessionConfiguration+Equatable.swift */, A3C7BADA27E4E82100BBF4FA /* WebSocketEngineError+Equatable.swift */, - A3BEB6B227F3245E00D6D80D /* XCTestCase+MockData.swift */, 84196FA22805892500185E99 /* LocalMessageState+Extensions.swift */, - A3C7BAA727E38C4A00BBF4FA /* Unique */, + A3BEB6B227F3245E00D6D80D /* XCTestCase+MockData.swift */, + 792E3DDE25CACFA80040B0C2 /* XCTestCase+TestImages.swift */, + 82F714A02B077F3300442A74 /* XCTestCase+iOS13.swift */, + 82F714A22B077FDE00442A74 /* XCTestCase+StressTest.swift */, + 82F714A82B0785D900442A74 /* XCTest+Helpers.swift */, ); path = Extensions; sourceTree = ""; @@ -7455,6 +7586,7 @@ A3C7BA8927E37E3D00BBF4FA /* RequestDecoder_Spy.swift */, A3C7BA8727E37DF000BBF4FA /* RequestEncoder_Spy.swift */, 8414A81B2767944B001BA9D7 /* RetryStrategy_Spy.swift */, + 82E655322B06748400D64906 /* Spy.swift */, ); path = Spy; sourceTree = ""; @@ -8657,7 +8789,7 @@ name = StreamChatUITests; packageProductDependencies = ( A3BD4814281A941B0090D511 /* AutomaticSettings */, - 8274181C2ACDE851004A23DA /* StreamSwiftTestHelpers */, + 82F714AA2B078AE800442A74 /* StreamSwiftTestHelpers */, ); productName = StreamChatUITests; productReference = 7908820525432B7200896F03 /* StreamChatUITests.xctest */; @@ -8708,7 +8840,6 @@ ); name = StreamChatTestTools; packageProductDependencies = ( - 827418182ACDE83A004A23DA /* StreamSwiftTestHelpers */, ); productName = StreamChatTestTools; productReference = 793060E625778896005CF846 /* StreamChatTestTools.framework */; @@ -8729,7 +8860,6 @@ ); name = StreamChatTestToolsTests; packageProductDependencies = ( - 827418162ACDE830004A23DA /* StreamSwiftTestHelpers */, ); productName = StreamChatTestToolsTests; productReference = 793060EE25778897005CF846 /* StreamChatTestToolsTests.xctest */; @@ -8773,7 +8903,6 @@ ); name = StreamChatTests; packageProductDependencies = ( - 8274181A2ACDE844004A23DA /* StreamSwiftTestHelpers */, ); productName = StreamChatClientTests; productReference = 799C9451247D59B1001F1104 /* StreamChatTests.xctest */; @@ -8862,8 +8991,6 @@ ); name = StreamChatTestMockServer; packageProductDependencies = ( - 827418142ACDE820004A23DA /* StreamSwiftTestHelpers */, - 8274142C2ACDEBAD009CD13C /* Swifter */, ); productName = StreamChatUITestTools; productReference = A3A0C999283E952900B18DA4 /* StreamChatTestMockServer.framework */; @@ -9114,7 +9241,6 @@ A3BD4869281FD4500090D511 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */, C1B49B39282283C100F4E89E /* XCRemoteSwiftPackageReference "GDPerformanceView-Swift" */, E334B384282468F2002E9640 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, - 8274142B2ACDEBAD009CD13C /* XCRemoteSwiftPackageReference "swifter" */, ); productRefGroup = 8AD5EC9022E9A3E8005CFAC9 /* Products */; projectDirPath = ""; @@ -10026,12 +10152,14 @@ A3C3BC1A27E87EFE00224761 /* DatabaseContainer_Spy.swift in Sources */, A311B43627E8BC8400CFCF6D /* ChannelMemberController_Delegate.swift in Sources */, A3C3BC7827E8AA9400224761 /* ChannelListQuery+Equatable.swift in Sources */, + 82E6553B2B0677EA00D64906 /* TestRunnerEnvironment.swift in Sources */, A3C3BC3B27E87F5100224761 /* EventMiddleware_Mock.swift in Sources */, A311B43727E8BC8400CFCF6D /* ChannelController_Delegate.swift in Sources */, A344077427D753530044F150 /* ChannelUnreadCount_Mock.swift in Sources */, A3C3BC3A27E87F5100224761 /* WebSocketPingController_Mock.swift in Sources */, A3C3BC5F27E8AA0A00224761 /* AttachmentAction+Unique.swift in Sources */, ADB951B0291C22E900800554 /* UploadedAttachment.swift in Sources */, + 82E655412B067A4C00D64906 /* WaitFor.swift in Sources */, A3C3BC8727E8AB6B00224761 /* JSONEncoder+Extensions.swift in Sources */, A3C3BC7527E8AA7000224761 /* Endpoint+Mock.swift in Sources */, A344077527D753530044F150 /* ChatChannel_Mock.swift in Sources */, @@ -10045,6 +10173,7 @@ A3C3BC3327E87F2900224761 /* OfflineRequestsRepository_Mock.swift in Sources */, A344078027D753530044F150 /* ChatChannelMember_Mock.swift in Sources */, C1C5345A29AFDDAE006F9AF4 /* ChannelRepository_Mock.swift in Sources */, + 82F714A32B077FDE00442A74 /* XCTestCase+StressTest.swift in Sources */, A3C3BC1B27E87EFE00224761 /* ChatClient_Mock.swift in Sources */, A3C3BC6727E8AA0A00224761 /* ChatMessage+Unique.swift in Sources */, A3C3BC3427E87F2900224761 /* SyncRepository_Mock.swift in Sources */, @@ -10068,6 +10197,7 @@ A311B43827E8BC8400CFCF6D /* WebSocketPingController_Delegate.swift in Sources */, A311B43227E8BC8400CFCF6D /* TestChannelListObserver.swift in Sources */, A3C3BC7927E8AA9400224761 /* AnyEncodable+Equatable.swift in Sources */, + 82E6553F2B06798100D64906 /* AssertJSONEqual.swift in Sources */, A344078C27D753530044F150 /* ChatChannelController_Mock.swift in Sources */, A3C3BC4927E87F5C00224761 /* ChannelMemberUpdater_Mock.swift in Sources */, A3C3BC4427E87F5C00224761 /* UserUpdater_Mock.swift in Sources */, @@ -10075,8 +10205,10 @@ A3C3BC3027E87F2900224761 /* MemberListController_Mock.swift in Sources */, A311B42D27E8BB7400CFCF6D /* StreamChatTestTools.swift in Sources */, 40D484022A1264F1009E4134 /* MockAudioRecorder.swift in Sources */, + 82F714A72B0784D900442A74 /* UnwrapAsync.swift in Sources */, A344077627D753530044F150 /* ChatMessageReaction_Mock.swift in Sources */, A311B43B27E8BC8400CFCF6D /* ChannelWatcherListController_Delegate.swift in Sources */, + 82E655332B06748400D64906 /* Spy.swift in Sources */, A344078627D753530044F150 /* MessagePayload.swift in Sources */, A3C3BC1F27E87F1200224761 /* ListDatabaseObserver_Mock.swift in Sources */, A3C3BC3227E87F2900224761 /* URLProtocol_Mock.swift in Sources */, @@ -10113,11 +10245,15 @@ A3C3BC2B27E87F2000224761 /* DatabaseSession_Mock.swift in Sources */, C17E0AF72B04D190007188F1 /* ListDatabaseObserverWrapper_Mock.swift in Sources */, A344077827D753530044F150 /* CurrentChatUser_Mock.swift in Sources */, + 82E6554B2B067ED700D64906 /* WaitUntil.swift in Sources */, A3C3BC6027E8AA0A00224761 /* Date+Unique.swift in Sources */, + 82F714A92B0785D900442A74 /* XCTest+Helpers.swift in Sources */, A3C3BC2827E87F2000224761 /* UserRequestBody.swift in Sources */, A3C3BC3F27E87F5C00224761 /* ChannelListUpdater_Spy.swift in Sources */, A3C3BC6427E8AA0A00224761 /* ChannelId+Unique.swift in Sources */, + 82F714A12B077F3300442A74 /* XCTestCase+iOS13.swift in Sources */, A3C3BC7127E8AA4300224761 /* TestFetchedResultsController.swift in Sources */, + 82E655392B06775D00D64906 /* MockFunc.swift in Sources */, A344078927D753530044F150 /* UserPayload.swift in Sources */, A3C3BC7327E8AA4300224761 /* TestItem.swift in Sources */, A344078427D753530044F150 /* MemberPayload.swift in Sources */, @@ -10139,6 +10275,7 @@ A3C3BC2227E87F1200224761 /* CurrentUserController_Mock.swift in Sources */, 40D4840C2A1264F1009E4134 /* MockAppStateObserver.swift in Sources */, A344078D27D753530044F150 /* ChatUserSearchController_Mock.swift in Sources */, + 82E655372B06756A00D64906 /* AssertTestQueue.swift in Sources */, A3C3BC4227E87F5C00224761 /* UserListUpdater_Mock.swift in Sources */, A344078227D753530044F150 /* NSManagedObject+ContextChange.swift in Sources */, A3C3BC2627E87F2000224761 /* TestAttachmentEnvelope.swift in Sources */, @@ -10151,12 +10288,15 @@ 40D4840B2A1264F1009E4134 /* MockAudioPlayerDelegate.swift in Sources */, A3C3BC4727E87F5C00224761 /* TypingEventsSender_Mock.swift in Sources */, A3C3BC3727E87F3200224761 /* Logger_Spy.swift in Sources */, + 82F714A52B07831700442A74 /* AssertDate.swift in Sources */, A3C3BC6B27E8AA4300224761 /* TestUser.swift in Sources */, A3C3BC2D27E87F2000224761 /* TestCustomEventPayload.swift in Sources */, A3C3BC9527E8AC0A00224761 /* RequestDecoder_Spy.swift in Sources */, A311B43027E8BC8400CFCF6D /* ChatUserController_Delegate.swift in Sources */, C163ED012992B378006D6124 /* NotificationExtensionLifecycle_Mock.swift in Sources */, + 82E655352B06751D00D64906 /* QueueAwareDelegate.swift in Sources */, A3C3BC6D27E8AA4300224761 /* TestManagedObject.swift in Sources */, + 8263464C2B0BACF600122D0E /* Difference.swift in Sources */, A3C3BC2427E87F1800224761 /* ChatChannelController_Spy.swift in Sources */, A3C3BC1927E87EFE00224761 /* ConnectionRepository_Mock.swift in Sources */, A3C3BC6827E8AA0A00224761 /* TypingEventDTO+Unique.swift in Sources */, @@ -10176,6 +10316,8 @@ A3C3BC2727E87F2000224761 /* ChatMessageAttachment.swift in Sources */, A3C3BC8527E8AB6200224761 /* Array+Subscript.swift in Sources */, 84196FA32805892500185E99 /* LocalMessageState+Extensions.swift in Sources */, + 82E655452B067CAE00D64906 /* AssertResult.swift in Sources */, + 82E655432B067C3600D64906 /* AssertAsync.swift in Sources */, A34ECB5C27F5D0BF00A804C1 /* TestDataModel2.xcdatamodeld in Sources */, A3C3BC1D27E87F0800224761 /* APIClient_Spy.swift in Sources */, 40D484072A1264F1009E4134 /* MockAudioRecordingDelegate.swift in Sources */, @@ -10909,22 +11051,42 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 826346632B0BAE3800122D0E /* String+File.swift in Sources */, A3A0C9A6283E955200B18DA4 /* WebsocketResponses.swift in Sources */, + 8263466A2B0BAE3800122D0E /* MimeTypes.swift in Sources */, + 826346642B0BAE3800122D0E /* HttpParser.swift in Sources */, A35715F4283E98110014E3B0 /* StreamChatTestMockServer.swift in Sources */, + 826346712B0BAE3800122D0E /* HttpRequest.swift in Sources */, + 826346702B0BAE3800122D0E /* HttpResponse.swift in Sources */, 827414432ACDF76C009CD13C /* Dictionary.swift in Sources */, A3A0C9A5283E955200B18DA4 /* TestData.swift in Sources */, - A3A0C9A3283E955200B18DA4 /* HttpServer.swift in Sources */, + A3A0C9A3283E955200B18DA4 /* Swifter.swift in Sources */, + 826346732B0BAE3800122D0E /* HttpServer.swift in Sources */, + 826346722B0BAE3800122D0E /* Process.swift in Sources */, 827414412ACDF6C2009CD13C /* String.swift in Sources */, 829CD5C52848C2EA003C3877 /* ParticipantRobot.swift in Sources */, + 826346622B0BAE3800122D0E /* Socket+File.swift in Sources */, + 826346682B0BAE3800122D0E /* String+SHA1.swift in Sources */, + 8263466D2B0BAE3800122D0E /* HttpRouter.swift in Sources */, + 8263466C2B0BAE3800122D0E /* WebSockets.swift in Sources */, A3A0C9B3283E955200B18DA4 /* EventResponses.swift in Sources */, A3A0C9A1283E955200B18DA4 /* ChannelResponses.swift in Sources */, + 826346672B0BAE3800122D0E /* Scopes.swift in Sources */, A3A0C9A9283E955200B18DA4 /* MessageResponses.swift in Sources */, + 826346742B0BAE3800122D0E /* Errno.swift in Sources */, 9041E4AD2AE9768800CA2A2A /* MembersResponse.swift in Sources */, + 8263466B2B0BAE3800122D0E /* Files.swift in Sources */, 826992C82900628500D2D470 /* DeviceRemoteControl.swift in Sources */, + 826346692B0BAE3800122D0E /* String+BASE64.swift in Sources */, A3A0C9AD283E955200B18DA4 /* MessageList.swift in Sources */, A3A0C9B1283E955200B18DA4 /* LaunchArgument.swift in Sources */, + 826346652B0BAE3800122D0E /* Socket+Server.swift in Sources */, A3A0C9B0283E955200B18DA4 /* StreamMockServer.swift in Sources */, A3A0C9B4283E955200B18DA4 /* MockServerAttributes.swift in Sources */, + 826346752B0BAE3800122D0E /* Socket.swift in Sources */, + 826346662B0BAE3800122D0E /* String+Misc.swift in Sources */, + 8263466F2B0BAE3800122D0E /* HttpServerIO.swift in Sources */, + 8263466E2B0BAE3800122D0E /* DemoServer.swift in Sources */, A3A0C9B2283E955200B18DA4 /* User.swift in Sources */, A3A0C9B5283E955200B18DA4 /* ReactionResponses.swift in Sources */, A3A0C9AA283E955200B18DA4 /* ChannelConfig.swift in Sources */, @@ -14006,14 +14168,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 8274142B2ACDEBAD009CD13C /* XCRemoteSwiftPackageReference "swifter" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/httpswift/swifter"; - requirement = { - kind = revision; - revision = 1e4f51c92d7ca486242d8bf0722b99de2c3531aa; - }; - }; A3BD4813281A941B0090D511 /* XCRemoteSwiftPackageReference "AutomaticSettings" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/krzysztofzablocki/AutomaticSettings.git"; @@ -14035,7 +14189,7 @@ repositoryURL = "https://github.com/GetStream/stream-chat-swift-test-helpers.git"; requirement = { kind = exactVersion; - version = 0.3.5; + version = 0.4.1; }; }; ADDFDE272779EC67003B3B07 /* XCRemoteSwiftPackageReference "atlantis" */ = { @@ -14073,42 +14227,17 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 8274142C2ACDEBAD009CD13C /* Swifter */ = { - isa = XCSwiftPackageProductDependency; - package = 8274142B2ACDEBAD009CD13C /* XCRemoteSwiftPackageReference "swifter" */; - productName = Swifter; - }; - 827418142ACDE820004A23DA /* StreamSwiftTestHelpers */ = { - isa = XCSwiftPackageProductDependency; - package = A3D15D8C27E9D839006B34D7 /* XCRemoteSwiftPackageReference "stream-chat-swift-test-helpers" */; - productName = StreamSwiftTestHelpers; - }; - 827418162ACDE830004A23DA /* StreamSwiftTestHelpers */ = { - isa = XCSwiftPackageProductDependency; - package = A3D15D8C27E9D839006B34D7 /* XCRemoteSwiftPackageReference "stream-chat-swift-test-helpers" */; - productName = StreamSwiftTestHelpers; - }; - 827418182ACDE83A004A23DA /* StreamSwiftTestHelpers */ = { - isa = XCSwiftPackageProductDependency; - package = A3D15D8C27E9D839006B34D7 /* XCRemoteSwiftPackageReference "stream-chat-swift-test-helpers" */; - productName = StreamSwiftTestHelpers; - }; - 8274181A2ACDE844004A23DA /* StreamSwiftTestHelpers */ = { - isa = XCSwiftPackageProductDependency; - package = A3D15D8C27E9D839006B34D7 /* XCRemoteSwiftPackageReference "stream-chat-swift-test-helpers" */; - productName = StreamSwiftTestHelpers; - }; - 8274181C2ACDE851004A23DA /* StreamSwiftTestHelpers */ = { + 8274181E2ACDE85E004A23DA /* StreamSwiftTestHelpers */ = { isa = XCSwiftPackageProductDependency; package = A3D15D8C27E9D839006B34D7 /* XCRemoteSwiftPackageReference "stream-chat-swift-test-helpers" */; productName = StreamSwiftTestHelpers; }; - 8274181E2ACDE85E004A23DA /* StreamSwiftTestHelpers */ = { + 827418202ACDE86F004A23DA /* StreamSwiftTestHelpers */ = { isa = XCSwiftPackageProductDependency; package = A3D15D8C27E9D839006B34D7 /* XCRemoteSwiftPackageReference "stream-chat-swift-test-helpers" */; productName = StreamSwiftTestHelpers; }; - 827418202ACDE86F004A23DA /* StreamSwiftTestHelpers */ = { + 82F714AA2B078AE800442A74 /* StreamSwiftTestHelpers */ = { isa = XCSwiftPackageProductDependency; package = A3D15D8C27E9D839006B34D7 /* XCRemoteSwiftPackageReference "stream-chat-swift-test-helpers" */; productName = StreamSwiftTestHelpers; diff --git a/TestTools/StreamChatTestMockServer/Extensions/Dictionary.swift b/TestTools/StreamChatTestMockServer/Extensions/Dictionary.swift index 6a24ef96c0f..0e234e18c8c 100644 --- a/TestTools/StreamChatTestMockServer/Extensions/Dictionary.swift +++ b/TestTools/StreamChatTestMockServer/Extensions/Dictionary.swift @@ -1,5 +1,5 @@ // -// Copyright © 2022 Stream.io Inc. All rights reserved. +// Copyright © 2023 Stream.io Inc. All rights reserved. // import Foundation diff --git a/TestTools/StreamChatTestMockServer/Extensions/String.swift b/TestTools/StreamChatTestMockServer/Extensions/String.swift index 6d26fecd4ff..5a3f3d267d8 100644 --- a/TestTools/StreamChatTestMockServer/Extensions/String.swift +++ b/TestTools/StreamChatTestMockServer/Extensions/String.swift @@ -1,5 +1,5 @@ // -// Copyright © 2022 Stream.io Inc. All rights reserved. +// Copyright © 2023 Stream.io Inc. All rights reserved. // import XCTest diff --git a/TestTools/StreamChatTestMockServer/Extensions/HttpServer.swift b/TestTools/StreamChatTestMockServer/Extensions/Swifter.swift similarity index 97% rename from TestTools/StreamChatTestMockServer/Extensions/HttpServer.swift rename to TestTools/StreamChatTestMockServer/Extensions/Swifter.swift index 89a07d7e66b..e54701cede3 100644 --- a/TestTools/StreamChatTestMockServer/Extensions/HttpServer.swift +++ b/TestTools/StreamChatTestMockServer/Extensions/Swifter.swift @@ -2,7 +2,6 @@ // Copyright © 2023 Stream.io Inc. All rights reserved. // -import Swifter import Foundation public extension HttpServer { diff --git a/TestTools/StreamChatTestMockServer/MockServer/AttachmentResponses.swift b/TestTools/StreamChatTestMockServer/MockServer/AttachmentResponses.swift index bfb231a560d..be28ffd6d7e 100644 --- a/TestTools/StreamChatTestMockServer/MockServer/AttachmentResponses.swift +++ b/TestTools/StreamChatTestMockServer/MockServer/AttachmentResponses.swift @@ -3,7 +3,6 @@ // @testable import StreamChat -import Swifter import XCTest public extension StreamMockServer { diff --git a/TestTools/StreamChatTestMockServer/MockServer/ChannelConfig.swift b/TestTools/StreamChatTestMockServer/MockServer/ChannelConfig.swift index 14405090928..4bee918ece0 100644 --- a/TestTools/StreamChatTestMockServer/MockServer/ChannelConfig.swift +++ b/TestTools/StreamChatTestMockServer/MockServer/ChannelConfig.swift @@ -5,7 +5,6 @@ import Foundation @testable import StreamChat -import Swifter import XCTest // MARK: - Config diff --git a/TestTools/StreamChatTestMockServer/MockServer/ChannelResponses.swift b/TestTools/StreamChatTestMockServer/MockServer/ChannelResponses.swift index 22098ea49a0..7bf966472ea 100644 --- a/TestTools/StreamChatTestMockServer/MockServer/ChannelResponses.swift +++ b/TestTools/StreamChatTestMockServer/MockServer/ChannelResponses.swift @@ -3,7 +3,6 @@ // @testable import StreamChat -import Swifter import XCTest public let channelKey = ChannelCodingKeys.self diff --git a/TestTools/StreamChatTestMockServer/MockServer/EventResponses.swift b/TestTools/StreamChatTestMockServer/MockServer/EventResponses.swift index 0edfde4a964..f6ea6dd9778 100644 --- a/TestTools/StreamChatTestMockServer/MockServer/EventResponses.swift +++ b/TestTools/StreamChatTestMockServer/MockServer/EventResponses.swift @@ -3,7 +3,6 @@ // @testable import StreamChat -import Swifter import XCTest public let eventKey = EventPayload.CodingKeys.self diff --git a/TestTools/StreamChatTestMockServer/MockServer/MembersResponse.swift b/TestTools/StreamChatTestMockServer/MockServer/MembersResponse.swift index fec66e716de..f1ba263d2e1 100644 --- a/TestTools/StreamChatTestMockServer/MockServer/MembersResponse.swift +++ b/TestTools/StreamChatTestMockServer/MockServer/MembersResponse.swift @@ -2,7 +2,6 @@ // Copyright © 2023 Stream.io Inc. All rights reserved. // -import Swifter public extension StreamMockServer { diff --git a/TestTools/StreamChatTestMockServer/MockServer/MessageResponses.swift b/TestTools/StreamChatTestMockServer/MockServer/MessageResponses.swift index 6fa19f3064a..5926bf3ac16 100644 --- a/TestTools/StreamChatTestMockServer/MockServer/MessageResponses.swift +++ b/TestTools/StreamChatTestMockServer/MockServer/MessageResponses.swift @@ -3,7 +3,6 @@ // @testable import StreamChat -import Swifter import XCTest public let messageKey = MessagePayloadsCodingKeys.self diff --git a/TestTools/StreamChatTestMockServer/MockServer/ReactionResponses.swift b/TestTools/StreamChatTestMockServer/MockServer/ReactionResponses.swift index 5c90b8d8a03..62c1b9222c7 100644 --- a/TestTools/StreamChatTestMockServer/MockServer/ReactionResponses.swift +++ b/TestTools/StreamChatTestMockServer/MockServer/ReactionResponses.swift @@ -3,7 +3,6 @@ // @testable import StreamChat -import Swifter import XCTest public let reactionKey = MessageReactionPayload.CodingKeys.self diff --git a/TestTools/StreamChatTestMockServer/MockServer/StreamMockServer.swift b/TestTools/StreamChatTestMockServer/MockServer/StreamMockServer.swift index f07fc4f0647..604f707e66d 100644 --- a/TestTools/StreamChatTestMockServer/MockServer/StreamMockServer.swift +++ b/TestTools/StreamChatTestMockServer/MockServer/StreamMockServer.swift @@ -3,7 +3,6 @@ // @testable import StreamChat -import Swifter import Foundation import XCTest diff --git a/TestTools/StreamChatTestMockServer/MockServer/User.swift b/TestTools/StreamChatTestMockServer/MockServer/User.swift index a863329d2c8..11589782804 100644 --- a/TestTools/StreamChatTestMockServer/MockServer/User.swift +++ b/TestTools/StreamChatTestMockServer/MockServer/User.swift @@ -3,7 +3,6 @@ // @testable import StreamChat -import Swifter public let userKey = UserPayloadsCodingKeys.self diff --git a/TestTools/StreamChatTestMockServer/MockServer/WebsocketResponses.swift b/TestTools/StreamChatTestMockServer/MockServer/WebsocketResponses.swift index 8f5c9f48edf..e2ec2bec129 100644 --- a/TestTools/StreamChatTestMockServer/MockServer/WebsocketResponses.swift +++ b/TestTools/StreamChatTestMockServer/MockServer/WebsocketResponses.swift @@ -4,7 +4,6 @@ @testable import StreamChat import Foundation -import Swifter public extension StreamMockServer { diff --git a/TestTools/StreamChatTestMockServer/Robots/ParticipantRobot.swift b/TestTools/StreamChatTestMockServer/Robots/ParticipantRobot.swift index 07678e9f638..6ca997ca1b2 100644 --- a/TestTools/StreamChatTestMockServer/Robots/ParticipantRobot.swift +++ b/TestTools/StreamChatTestMockServer/Robots/ParticipantRobot.swift @@ -3,7 +3,6 @@ // @testable import StreamChat -import Swifter import XCTest public class ParticipantRobot { diff --git a/TestTools/StreamChatTestMockServer/Swifter/DemoServer.swift b/TestTools/StreamChatTestMockServer/Swifter/DemoServer.swift new file mode 100644 index 00000000000..495821473d0 --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/DemoServer.swift @@ -0,0 +1,205 @@ +// +// DemoServer.swift +// Swifter +// +// Copyright (c) 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +// swiftlint:disable function_body_length +public func demoServer(_ publicDir: String) -> HttpServer { + + print(publicDir) + + let server = HttpServer() + + server["/public/:path"] = shareFilesFromDirectory(publicDir) + + server["/files/:path"] = directoryBrowser("/") + + server["/"] = scopes { + html { + body { + ul(server.routes) { service in + li { + a { href = service; inner = service } + } + } + } + } + } + + server["/magic"] = { .ok(.htmlBody("You asked for " + $0.path), ["XXX-Custom-Header": "value"]) } + + server["/test/:param1/:param2"] = { request in + scopes { + html { + body { + h3 { inner = "Address: \(request.address ?? "unknown")" } + h3 { inner = "Url: \(request.path)" } + h3 { inner = "Method: \(request.method)" } + + h3 { inner = "Query:" } + + table(request.queryParams) { param in + tr { + td { inner = param.0 } + td { inner = param.1 } + } + } + + h3 { inner = "Headers:" } + + table(request.headers) { header in + tr { + td { inner = header.0 } + td { inner = header.1 } + } + } + + h3 { inner = "Route params:" } + + table(request.params) { param in + tr { + td { inner = param.0 } + td { inner = param.1 } + } + } + } + } + }(request) + } + + server.GET["/upload"] = scopes { + html { + body { + form { + method = "POST" + action = "/upload" + enctype = "multipart/form-data" + + input { name = "my_file1"; type = "file" } + input { name = "my_file2"; type = "file" } + input { name = "my_file3"; type = "file" } + + button { + type = "submit" + inner = "Upload" + } + } + } + } + } + + server.POST["/upload"] = { request in + var response = "" + for multipart in request.parseMultiPartFormData() { + guard let name = multipart.name, let fileName = multipart.fileName else { continue } + response += "Name: \(name) File name: \(fileName) Size: \(multipart.body.count)
" + } + return HttpResponse.ok(.htmlBody(response), ["XXX-Custom-Header": "value"]) + } + + server.GET["/login"] = scopes { + html { + head { + script { src = "http://cdn.staticfile.org/jquery/2.1.4/jquery.min.js" } + stylesheet { href = "http://cdn.staticfile.org/twitter-bootstrap/3.3.0/css/bootstrap.min.css" } + } + body { + h3 { inner = "Sign In" } + + form { + method = "POST" + action = "/login" + + fieldset { + input { placeholder = "E-mail"; name = "email"; type = "email"; autofocus = "" } + input { placeholder = "Password"; name = "password"; type = "password"; autofocus = "" } + a { + href = "/login" + button { + type = "submit" + inner = "Login" + } + } + } + + } + javascript { + src = "http://cdn.staticfile.org/twitter-bootstrap/3.3.0/js/bootstrap.min.js" + } + } + } + } + + server.POST["/login"] = { request in + let formFields = request.parseUrlencodedForm() + return HttpResponse.ok(.htmlBody(formFields.map({ "\($0.0) = \($0.1)" }).joined(separator: "
")), ["XXX-Custom-Header": "value"]) + } + + server["/demo"] = scopes { + html { + body { + center { + h2 { inner = "Hello Swift" } + img { src = "https://devimages.apple.com.edgekey.net/swift/images/swift-hero_2x.png" } + } + } + } + } + + server["/raw"] = { _ in + return HttpResponse.raw(200, "OK", ["XXX-Custom-Header": "value"], { try $0.write([UInt8]("test".utf8)) }) + } + + server["/redirect/permanently"] = { _ in + return .movedPermanently("http://www.google.com") + } + + server["/redirect/temporarily"] = { _ in + return .movedTemporarily("http://www.google.com") + } + + server["/long"] = { _ in + var longResponse = "" + for index in 0..<1000 { longResponse += "(\(index)),->" } + return .ok(.htmlBody(longResponse), ["XXX-Custom-Header": "value"]) + } + + server["/wildcard/*/test/*/:param"] = { request in + return .ok(.htmlBody(request.path), ["XXX-Custom-Header": "value"]) + } + + server["/stream"] = { _ in + return HttpResponse.raw(200, "OK", nil, { writer in + for index in 0...100 { + try writer.write([UInt8]("[chunk \(index)]".utf8)) + } + }) + } + + server["/websocket-echo"] = websocket(text: { (session, text) in + session.writeText(text) + }, binary: { (session, binary) in + session.writeBinary(binary) + }, pong: { (_, _) in + // Got a pong frame + }, connected: { _ in + // New client connected + }, disconnected: { _ in + // Client disconnected + }) + + server.notFoundHandler = { _ in + return .movedPermanently("https://github.com/404") + } + + server.middleware.append { request in + print("Middleware: \(request.address ?? "unknown address") -> \(request.method) -> \(request.path)") + return nil + } + + return server +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/Errno.swift b/TestTools/StreamChatTestMockServer/Swifter/Errno.swift new file mode 100644 index 00000000000..3657b95ee1c --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/Errno.swift @@ -0,0 +1,16 @@ +// +// Errno.swift +// Swifter +// +// Copyright © 2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +public class Errno { + + public class func description() -> String { + // https://forums.developer.apple.com/thread/113919 + return String(cString: strerror(errno)) + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/Files.swift b/TestTools/StreamChatTestMockServer/Swifter/Files.swift new file mode 100644 index 00000000000..a45e2c5b48e --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/Files.swift @@ -0,0 +1,106 @@ +// +// HttpHandlers+Files.swift +// Swifter +// +// Copyright (c) 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +public func shareFile(_ path: String) -> ((HttpRequest) -> HttpResponse) { + return { _ in + if let file = try? path.openForReading() { + let mimeType = path.mimeType() + var responseHeader: [String: String] = ["Content-Type": mimeType] + + if let attr = try? FileManager.default.attributesOfItem(atPath: path), + let fileSize = attr[FileAttributeKey.size] as? UInt64 { + responseHeader["Content-Length"] = String(fileSize) + } + return .raw(200, "OK", responseHeader, { writer in + try? writer.write(file) + file.close() + }) + } + return .notFound() + } +} + +public func shareFilesFromDirectory(_ directoryPath: String, defaults: [String] = ["index.html", "default.html"]) -> ((HttpRequest) -> HttpResponse) { + return { request in + guard let fileRelativePath = request.params.first else { + return .notFound() + } + if fileRelativePath.value.isEmpty { + for path in defaults { + if let file = try? (directoryPath + String.pathSeparator + path).openForReading() { + return .raw(200, "OK", [:], { writer in + try? writer.write(file) + file.close() + }) + } + } + } + let filePath = directoryPath + String.pathSeparator + fileRelativePath.value + + if let file = try? filePath.openForReading() { + let mimeType = fileRelativePath.value.mimeType() + var responseHeader: [String: String] = ["Content-Type": mimeType] + + if let attr = try? FileManager.default.attributesOfItem(atPath: filePath), + let fileSize = attr[FileAttributeKey.size] as? UInt64 { + responseHeader["Content-Length"] = String(fileSize) + } + + return .raw(200, "OK", responseHeader, { writer in + try? writer.write(file) + file.close() + }) + } + return .notFound() + } +} + +public func directoryBrowser(_ dir: String) -> ((HttpRequest) -> HttpResponse) { + return { request in + guard let (_, value) = request.params.first else { + return .notFound() + } + let filePath = dir + String.pathSeparator + value + do { + guard try filePath.exists() else { + return .notFound() + } + if try filePath.directory() { + var files = try filePath.files() + files.sort(by: {$0.lowercased() < $1.lowercased()}) + return scopes { + html { + body { + table(files) { file in + tr { + td { + a { + href = request.path + "/" + file + inner = file + } + } + } + } + } + } + }(request) + } else { + guard let file = try? filePath.openForReading() else { + return .notFound() + } + return .raw(200, "OK", [:], { writer in + try? writer.write(file) + file.close() + }) + } + } catch { + return HttpResponse.internalServerError(.text("Internal Server Error")) + } + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/HttpParser.swift b/TestTools/StreamChatTestMockServer/Swifter/HttpParser.swift new file mode 100644 index 00000000000..5c1abfa9e24 --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/HttpParser.swift @@ -0,0 +1,64 @@ +// +// HttpParser.swift +// Swifter +// +// Copyright (c) 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +enum HttpParserError: Error, Equatable { + case invalidStatusLine(String) + case negativeContentLength +} + +public class HttpParser { + + public init() { } + + public func readHttpRequest(_ socket: Socket) throws -> HttpRequest { + let statusLine = try socket.readLine() + let statusLineTokens = statusLine.components(separatedBy: " ") + if statusLineTokens.count < 3 { + throw HttpParserError.invalidStatusLine(statusLine) + } + let request = HttpRequest() + request.method = statusLineTokens[0] + let encodedPath = statusLineTokens[1].addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? statusLineTokens[1] + let urlComponents = URLComponents(string: encodedPath) + request.path = urlComponents?.path ?? "" + request.queryParams = urlComponents?.queryItems?.map { ($0.name, $0.value ?? "") } ?? [] + request.headers = try readHeaders(socket) + if let contentLength = request.headers["content-length"], let contentLengthValue = Int(contentLength) { + // Prevent a buffer overflow and runtime error trying to create an `UnsafeMutableBufferPointer` with + // a negative length + guard contentLengthValue >= 0 else { + throw HttpParserError.negativeContentLength + } + request.body = try readBody(socket, size: contentLengthValue) + } + return request + } + + private func readBody(_ socket: Socket, size: Int) throws -> [UInt8] { + return try socket.read(length: size) + } + + private func readHeaders(_ socket: Socket) throws -> [String: String] { + var headers = [String: String]() + while case let headerLine = try socket.readLine(), !headerLine.isEmpty { + let headerTokens = headerLine.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true).map(String.init) + if let name = headerTokens.first, let value = headerTokens.last { + headers[name.lowercased()] = value.trimmingCharacters(in: .whitespaces) + } + } + return headers + } + + func supportsKeepAlive(_ headers: [String: String]) -> Bool { + if let value = headers["connection"] { + return "keep-alive" == value.trimmingCharacters(in: .whitespaces) + } + return false + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/HttpRequest.swift b/TestTools/StreamChatTestMockServer/Swifter/HttpRequest.swift new file mode 100644 index 00000000000..00d0962f30b --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/HttpRequest.swift @@ -0,0 +1,173 @@ +// +// HttpRequest.swift +// Swifter +// +// Copyright (c) 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +public class HttpRequest { + + public var path: String = "" + public var queryParams: [(String, String)] = [] + public var method: String = "" + public var headers: [String: String] = [:] + public var body: [UInt8] = [] + public var address: String? = "" + public var params: [String: String] = [:] + + public init() {} + + public func hasTokenForHeader(_ headerName: String, token: String) -> Bool { + guard let headerValue = headers[headerName] else { + return false + } + return headerValue.components(separatedBy: ",").filter({ $0.trimmingCharacters(in: .whitespaces).lowercased() == token }).count > 0 + } + + public func parseUrlencodedForm() -> [(String, String)] { + guard let contentTypeHeader = headers["content-type"] else { + return [] + } + let contentTypeHeaderTokens = contentTypeHeader.components(separatedBy: ";").map { $0.trimmingCharacters(in: .whitespaces) } + guard let contentType = contentTypeHeaderTokens.first, contentType == "application/x-www-form-urlencoded" else { + return [] + } + guard let utf8String = String(bytes: body, encoding: .utf8) else { + // Consider to throw an exception here (examine the encoding from headers). + return [] + } + return utf8String.components(separatedBy: "&").map { param -> (String, String) in + let tokens = param.components(separatedBy: "=") + if let name = tokens.first?.removingPercentEncoding, let value = tokens.last?.removingPercentEncoding, tokens.count == 2 { + return (name.replacingOccurrences(of: "+", with: " "), + value.replacingOccurrences(of: "+", with: " ")) + } + return ("", "") + } + } + + public struct MultiPart { + + public let headers: [String: String] + public let body: [UInt8] + + public var name: String? { + return valueFor("content-disposition", parameter: "name")?.unquote() + } + + public var fileName: String? { + return valueFor("content-disposition", parameter: "filename")?.unquote() + } + + private func valueFor(_ headerName: String, parameter: String) -> String? { + return headers.reduce([String]()) { (combined, header: (key: String, value: String)) -> [String] in + guard header.key == headerName else { + return combined + } + let headerValueParams = header.value.components(separatedBy: ";").map { $0.trimmingCharacters(in: .whitespaces) } + return headerValueParams.reduce(combined, { (results, token) -> [String] in + let parameterTokens = token.components(separatedBy: "=") + if parameterTokens.first == parameter, let value = parameterTokens.last { + return results + [value] + } + return results + }) + }.first + } + } + + public func parseMultiPartFormData() -> [MultiPart] { + guard let contentTypeHeader = headers["content-type"] else { + return [] + } + let contentTypeHeaderTokens = contentTypeHeader.components(separatedBy: ";").map { $0.trimmingCharacters(in: .whitespaces) } + guard let contentType = contentTypeHeaderTokens.first, contentType == "multipart/form-data" else { + return [] + } + var boundary: String? + contentTypeHeaderTokens.forEach({ + let tokens = $0.components(separatedBy: "=") + if let key = tokens.first, key == "boundary" && tokens.count == 2 { + boundary = tokens.last + } + }) + if let boundary = boundary, boundary.utf8.count > 0 { + return parseMultiPartFormData(body, boundary: "--\(boundary)") + } + return [] + } + + private func parseMultiPartFormData(_ data: [UInt8], boundary: String) -> [MultiPart] { + var generator = data.makeIterator() + var result = [MultiPart]() + while let part = nextMultiPart(&generator, boundary: boundary, isFirst: result.isEmpty) { + result.append(part) + } + return result + } + + private func nextMultiPart(_ generator: inout IndexingIterator<[UInt8]>, boundary: String, isFirst: Bool) -> MultiPart? { + if isFirst { + guard nextUTF8MultiPartLine(&generator) == boundary else { + return nil + } + } else { + let /* ignore */ _ = nextUTF8MultiPartLine(&generator) + } + var headers = [String: String]() + while let line = nextUTF8MultiPartLine(&generator), !line.isEmpty { + let tokens = line.components(separatedBy: ":") + if let name = tokens.first, let value = tokens.last, tokens.count == 2 { + headers[name.lowercased()] = value.trimmingCharacters(in: .whitespaces) + } + } + guard let body = nextMultiPartBody(&generator, boundary: boundary) else { + return nil + } + return MultiPart(headers: headers, body: body) + } + + private func nextUTF8MultiPartLine(_ generator: inout IndexingIterator<[UInt8]>) -> String? { + var temp = [UInt8]() + while let value = generator.next() { + if value > HttpRequest.CR { + temp.append(value) + } + if value == HttpRequest.NL { + break + } + } + return String(bytes: temp, encoding: String.Encoding.utf8) + } + + // swiftlint:disable identifier_name + static let CR = UInt8(13) + static let NL = UInt8(10) + + private func nextMultiPartBody(_ generator: inout IndexingIterator<[UInt8]>, boundary: String) -> [UInt8]? { + var body = [UInt8]() + let boundaryArray = [UInt8](boundary.utf8) + var matchOffset = 0 + while let x = generator.next() { + matchOffset = ( x == boundaryArray[matchOffset] ? matchOffset + 1 : 0 ) + body.append(x) + if matchOffset == boundaryArray.count { + #if swift(>=4.2) + body.removeSubrange(body.count-matchOffset ..< body.count) + #else + body.removeSubrange(CountableRange(body.count-matchOffset ..< body.count)) + #endif + if body.last == HttpRequest.NL { + body.removeLast() + if body.last == HttpRequest.CR { + body.removeLast() + } + } + return body + } + } + return nil + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/HttpResponse.swift b/TestTools/StreamChatTestMockServer/Swifter/HttpResponse.swift new file mode 100644 index 00000000000..cef8aa41c0f --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/HttpResponse.swift @@ -0,0 +1,190 @@ +// +// HttpResponse.swift +// Swifter +// +// Copyright (c) 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +public enum SerializationError: Error { + case invalidObject + case notSupported +} + +public protocol HttpResponseBodyWriter { + func write(_ file: String.File) throws + func write(_ data: [UInt8]) throws + func write(_ data: ArraySlice) throws + func write(_ data: NSData) throws + func write(_ data: Data) throws +} + +public enum HttpResponseBody { + + case json(Any) + case html(String) + case htmlBody(String) + case text(String) + case data(Data, contentType: String? = nil) + case custom(Any, (Any) throws -> String) + + func content() -> (Int, ((HttpResponseBodyWriter) throws -> Void)?) { + do { + switch self { + case .json(let object): + guard JSONSerialization.isValidJSONObject(object) else { + throw SerializationError.invalidObject + } + let data = try JSONSerialization.data(withJSONObject: object) + return (data.count, { + try $0.write(data) + }) + case .text(let body): + let data = [UInt8](body.utf8) + return (data.count, { + try $0.write(data) + }) + case .html(let html): + let data = [UInt8](html.utf8) + return (data.count, { + try $0.write(data) + }) + case .htmlBody(let body): + let serialized = "\(body)" + let data = [UInt8](serialized.utf8) + return (data.count, { + try $0.write(data) + }) + case .data(let data, _): + return (data.count, { + try $0.write(data) + }) + case .custom(let object, let closure): + let serialized = try closure(object) + let data = [UInt8](serialized.utf8) + return (data.count, { + try $0.write(data) + }) + } + } catch { + let data = [UInt8]("Serialization error: \(error)".utf8) + return (data.count, { + try $0.write(data) + }) + } + } +} + +// swiftlint:disable cyclomatic_complexity +public enum HttpResponse { + + case switchProtocols([String: String], (Socket) -> Void) + case ok(HttpResponseBody, [String: String] = [:]), created, accepted + case movedPermanently(String) + case movedTemporarily(String) + case badRequest(HttpResponseBody?), unauthorized(HttpResponseBody?), forbidden(HttpResponseBody?), notFound(HttpResponseBody? = nil), notAcceptable(HttpResponseBody?), tooManyRequests(HttpResponseBody?), internalServerError(HttpResponseBody?) + case raw(Int, String, [String: String]?, ((HttpResponseBodyWriter) throws -> Void)? ) + + public var statusCode: Int { + switch self { + case .switchProtocols : return 101 + case .ok : return 200 + case .created : return 201 + case .accepted : return 202 + case .movedPermanently : return 301 + case .movedTemporarily : return 307 + case .badRequest : return 400 + case .unauthorized : return 401 + case .forbidden : return 403 + case .notFound : return 404 + case .notAcceptable : return 406 + case .tooManyRequests : return 429 + case .internalServerError : return 500 + case .raw(let code, _, _, _) : return code + } + } + + public var reasonPhrase: String { + switch self { + case .switchProtocols : return "Switching Protocols" + case .ok : return "OK" + case .created : return "Created" + case .accepted : return "Accepted" + case .movedPermanently : return "Moved Permanently" + case .movedTemporarily : return "Moved Temporarily" + case .badRequest : return "Bad Request" + case .unauthorized : return "Unauthorized" + case .forbidden : return "Forbidden" + case .notFound : return "Not Found" + case .notAcceptable : return "Not Acceptable" + case .tooManyRequests : return "Too Many Requests" + case .internalServerError : return "Internal Server Error" + case .raw(_, let phrase, _, _) : return phrase + } + } + + public func headers() -> [String: String] { + var headers = ["Server": "Swifter \(HttpServer.VERSION)"] + switch self { + case .switchProtocols(let switchHeaders, _): + for (key, value) in switchHeaders { + headers[key] = value + } + case .ok(let body, let customHeaders): + for (key, value) in customHeaders { + headers.updateValue(value, forKey: key) + } + switch body { + case .json: headers["Content-Type"] = "application/json" + case .html, .htmlBody: headers["Content-Type"] = "text/html" + case .text: headers["Content-Type"] = "text/plain" + case .data(_, let contentType): headers["Content-Type"] = contentType + default:break + } + case .movedPermanently(let location): + headers["Location"] = location + case .movedTemporarily(let location): + headers["Location"] = location + case .raw(_, _, let rawHeaders, _): + if let rawHeaders = rawHeaders { + for (key, value) in rawHeaders { + headers.updateValue(value, forKey: key) + } + } + default:break + } + return headers + } + + func content() -> (length: Int, write: ((HttpResponseBodyWriter) throws -> Void)?) { + switch self { + case .ok(let body, _) : return body.content() + case .badRequest(let body), .unauthorized(let body), .forbidden(let body), .notFound(let body), .tooManyRequests(let body), .internalServerError(let body) : return body?.content() ?? (-1, nil) + case .raw(_, _, _, let writer) : return (-1, writer) + default : return (-1, nil) + } + } + + func socketSession() -> ((Socket) -> Void)? { + switch self { + case .switchProtocols(_, let handler) : return handler + default: return nil + } + } +} + +/** + Makes it possible to compare handler responses with '==', but + ignores any associated values. This should generally be what + you want. E.g.: + + let resp = handler(updatedRequest) + if resp == .NotFound { + print("Client requested not found: \(request.url)") + } +*/ + +func == (inLeft: HttpResponse, inRight: HttpResponse) -> Bool { + return inLeft.statusCode == inRight.statusCode +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/HttpRouter.swift b/TestTools/StreamChatTestMockServer/Swifter/HttpRouter.swift new file mode 100644 index 00000000000..1429627e47a --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/HttpRouter.swift @@ -0,0 +1,192 @@ +// +// HttpRouter.swift +// Swifter +// +// Copyright (c) 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +open class HttpRouter { + + public init() {} + + private class Node { + + /// The children nodes that form the route + var nodes = [String: Node]() + + /// Define whether or not this node is the end of a route + var isEndOfRoute: Bool = false + + /// The closure to handle the route + var handler: ((HttpRequest) -> HttpResponse)? + } + + private var rootNode = Node() + + /// The Queue to handle the thread safe access to the routes + private let queue = DispatchQueue(label: "swifter.httpserverio.httprouter") + + public func routes() -> [String] { + var routes = [String]() + for (_, child) in rootNode.nodes { + routes.append(contentsOf: routesForNode(child)) + } + return routes + } + + private func routesForNode(_ node: Node, prefix: String = "") -> [String] { + var result = [String]() + if node.handler != nil { + result.append(prefix) + } + for (key, child) in node.nodes { + result.append(contentsOf: routesForNode(child, prefix: prefix + "/" + key)) + } + return result + } + + public func register(_ method: String?, path: String, handler: ((HttpRequest) -> HttpResponse)?) { + var pathSegments = stripQuery(path).split("/") + if let method = method { + pathSegments.insert(method, at: 0) + } else { + pathSegments.insert("*", at: 0) + } + var pathSegmentsGenerator = pathSegments.makeIterator() + inflate(&rootNode, generator: &pathSegmentsGenerator).handler = handler + } + + public func route(_ method: String?, path: String) -> ([String: String], (HttpRequest) -> HttpResponse)? { + + return queue.sync { + if let method = method { + let pathSegments = (method + "/" + stripQuery(path)).split("/") + var pathSegmentsGenerator = pathSegments.makeIterator() + var params = [String: String]() + if let handler = findHandler(&rootNode, params: ¶ms, generator: &pathSegmentsGenerator) { + return (params, handler) + } + } + + let pathSegments = ("*/" + stripQuery(path)).split("/") + var pathSegmentsGenerator = pathSegments.makeIterator() + var params = [String: String]() + if let handler = findHandler(&rootNode, params: ¶ms, generator: &pathSegmentsGenerator) { + return (params, handler) + } + + return nil + } + } + + private func inflate(_ node: inout Node, generator: inout IndexingIterator<[String]>) -> Node { + + var currentNode = node + + while let pathSegment = generator.next() { + if let nextNode = currentNode.nodes[pathSegment] { + currentNode = nextNode + } else { + currentNode.nodes[pathSegment] = Node() + currentNode = currentNode.nodes[pathSegment]! + } + } + + currentNode.isEndOfRoute = true + return currentNode + } + + private func findHandler(_ node: inout Node, params: inout [String: String], generator: inout IndexingIterator<[String]>) -> ((HttpRequest) -> HttpResponse)? { + + var matchedRoutes = [Node]() + let pattern = generator.map { $0 } + let numberOfElements = pattern.count + + findHandler(&node, params: ¶ms, pattern: pattern, matchedNodes: &matchedRoutes, index: 0, count: numberOfElements) + return matchedRoutes.first?.handler + } + + // swiftlint:disable function_parameter_count + /// Find the handlers for a specified route + /// + /// - Parameters: + /// - node: The root node of the tree representing all the routes + /// - params: The parameters of the match + /// - pattern: The pattern or route to find in the routes tree + /// - matchedNodes: An array with the nodes matching the route + /// - index: The index of current position in the generator + /// - count: The number of elements if the route to match + private func findHandler(_ node: inout Node, params: inout [String: String], pattern: [String], matchedNodes: inout [Node], index: Int, count: Int) { + + if index < count, let pathToken = pattern[index].removingPercentEncoding { + + var currentIndex = index + 1 + let variableNodes = node.nodes.filter { $0.0.first == ":" } + if let variableNode = variableNodes.first { + if currentIndex == count && variableNode.1.isEndOfRoute { + // if it's the last element of the pattern and it's a variable, stop the search and + // append a tail as a value for the variable. + let tail = pattern[currentIndex.. 0 { + params[variableNode.0] = pathToken + "/" + tail + } else { + params[variableNode.0] = pathToken + } + + matchedNodes.append(variableNode.value) + return + } + params[variableNode.0] = pathToken + findHandler(&node.nodes[variableNode.0]!, params: ¶ms, pattern: pattern, matchedNodes: &matchedNodes, index: currentIndex, count: count) + } + + if var node = node.nodes[pathToken] { + findHandler(&node, params: ¶ms, pattern: pattern, matchedNodes: &matchedNodes, index: currentIndex, count: count) + } + + if var node = node.nodes["*"] { + findHandler(&node, params: ¶ms, pattern: pattern, matchedNodes: &matchedNodes, index: currentIndex, count: count) + } + + if let startStarNode = node.nodes["**"] { + if startStarNode.isEndOfRoute { + // ** at the end of a route works as a catch-all + matchedNodes.append(startStarNode) + return + } + + let startStarNodeKeys = startStarNode.nodes.keys + currentIndex += 1 + while currentIndex < count, let pathToken = pattern[currentIndex].removingPercentEncoding { + currentIndex += 1 + if startStarNodeKeys.contains(pathToken) { + findHandler(&startStarNode.nodes[pathToken]!, params: ¶ms, pattern: pattern, matchedNodes: &matchedNodes, index: currentIndex, count: count) + } + } + } + } + + if node.isEndOfRoute && index == count { + // if it's the last element and the path to match is done then it's a pattern matching + matchedNodes.append(node) + return + } + } + + private func stripQuery(_ path: String) -> String { + if let path = path.components(separatedBy: "?").first { + return path + } + return path + } +} + +extension String { + + func split(_ separator: Character) -> [String] { + return self.split { $0 == separator }.map(String.init) + } + +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/HttpServer.swift b/TestTools/StreamChatTestMockServer/Swifter/HttpServer.swift new file mode 100644 index 00000000000..b8d56790a9d --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/HttpServer.swift @@ -0,0 +1,84 @@ +// +// HttpServer.swift +// Swifter +// +// Copyright (c) 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +open class HttpServer: HttpServerIO { + + public static let VERSION: String = { + + #if os(Linux) + return "1.5.0" + #else + let bundle = Bundle(for: HttpServer.self) + guard let version = bundle.infoDictionary?["CFBundleShortVersionString"] as? String else { return "Unspecified" } + return version + #endif + }() + + private let router = HttpRouter() + + public override init() { + self.DELETE = MethodRoute(method: "DELETE", router: router) + self.PATCH = MethodRoute(method: "PATCH", router: router) + self.HEAD = MethodRoute(method: "HEAD", router: router) + self.POST = MethodRoute(method: "POST", router: router) + self.GET = MethodRoute(method: "GET", router: router) + self.PUT = MethodRoute(method: "PUT", router: router) + + self.delete = MethodRoute(method: "DELETE", router: router) + self.patch = MethodRoute(method: "PATCH", router: router) + self.head = MethodRoute(method: "HEAD", router: router) + self.post = MethodRoute(method: "POST", router: router) + self.get = MethodRoute(method: "GET", router: router) + self.put = MethodRoute(method: "PUT", router: router) + } + + public var DELETE, PATCH, HEAD, POST, GET, PUT: MethodRoute + public var delete, patch, head, post, get, put: MethodRoute + + public subscript(path: String) -> ((HttpRequest) -> HttpResponse)? { + get { return nil } + set { + router.register(nil, path: path, handler: newValue) + } + } + + public var routes: [String] { + return router.routes() + } + + public var notFoundHandler: ((HttpRequest) -> HttpResponse)? + + public var middleware = [(HttpRequest) -> HttpResponse?]() + + override open func dispatch(_ request: HttpRequest) -> ([String: String], (HttpRequest) -> HttpResponse) { + for layer in middleware { + if let response = layer(request) { + return ([:], { _ in response }) + } + } + if let result = router.route(request.method, path: request.path) { + return result + } + if let notFoundHandler = self.notFoundHandler { + return ([:], notFoundHandler) + } + return super.dispatch(request) + } + + public struct MethodRoute { + public let method: String + public let router: HttpRouter + public subscript(path: String) -> ((HttpRequest) -> HttpResponse)? { + get { return nil } + set { + router.register(method, path: path, handler: newValue) + } + } + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/HttpServerIO.swift b/TestTools/StreamChatTestMockServer/Swifter/HttpServerIO.swift new file mode 100644 index 00000000000..65c6e95a3e1 --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/HttpServerIO.swift @@ -0,0 +1,204 @@ +// +// HttpServer.swift +// Swifter +// +// Copyright (c) 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation +import Dispatch + +public protocol HttpServerIODelegate: AnyObject { + func socketConnectionReceived(_ socket: Socket) +} + +open class HttpServerIO { + + public weak var delegate: HttpServerIODelegate? + + private var socket = Socket(socketFileDescriptor: -1) + private var sockets = Set() + + public enum HttpServerIOState: Int32 { + case starting + case running + case stopping + case stopped + } + + private var stateValue: Int32 = HttpServerIOState.stopped.rawValue + + public private(set) var state: HttpServerIOState { + get { + return HttpServerIOState(rawValue: stateValue)! + } + set(state) { + #if !os(Linux) + OSAtomicCompareAndSwapInt(self.state.rawValue, state.rawValue, &stateValue) + #else + self.stateValue = state.rawValue + #endif + } + } + + public var operating: Bool { return self.state == .running } + + /// String representation of the IPv4 address to receive requests from. + /// It's only used when the server is started with `forceIPv4` option set to true. + /// Otherwise, `listenAddressIPv6` will be used. + public var listenAddressIPv4: String? + + /// String representation of the IPv6 address to receive requests from. + /// It's only used when the server is started with `forceIPv4` option set to false. + /// Otherwise, `listenAddressIPv4` will be used. + public var listenAddressIPv6: String? + + private let queue = DispatchQueue(label: "swifter.httpserverio.clientsockets") + + public func port() throws -> Int { + return Int(try socket.port()) + } + + public func isIPv4() throws -> Bool { + return try socket.isIPv4() + } + + deinit { + stop() + } + + @available(macOS 10.10, *) + public func start(_ port: in_port_t = 8080, forceIPv4: Bool = false, priority: DispatchQoS.QoSClass = DispatchQoS.QoSClass.background) throws { + guard !self.operating else { return } + stop() + self.state = .starting + let address = forceIPv4 ? listenAddressIPv4 : listenAddressIPv6 + self.socket = try Socket.tcpSocketForListen(port, forceIPv4, SOMAXCONN, address) + self.state = .running + DispatchQueue.global(qos: priority).async { [weak self] in + guard let strongSelf = self else { return } + guard strongSelf.operating else { return } + while let socket = try? strongSelf.socket.acceptClientSocket() { + DispatchQueue.global(qos: priority).async { [weak self] in + guard let strongSelf = self else { return } + guard strongSelf.operating else { return } + strongSelf.queue.async { + strongSelf.sockets.insert(socket) + } + + strongSelf.handleConnection(socket) + + strongSelf.queue.async { + strongSelf.sockets.remove(socket) + } + } + } + strongSelf.stop() + } + } + + public func stop() { + guard self.operating else { return } + self.state = .stopping + // Shutdown connected peers because they can live in 'keep-alive' or 'websocket' loops. + for socket in self.sockets { + socket.close() + } + self.queue.sync { + self.sockets.removeAll(keepingCapacity: true) + } + socket.close() + self.state = .stopped + } + + open func dispatch(_ request: HttpRequest) -> ([String: String], (HttpRequest) -> HttpResponse) { + return ([:], { _ in HttpResponse.notFound(nil) }) + } + + private func handleConnection(_ socket: Socket) { + let parser = HttpParser() + while self.operating, let request = try? parser.readHttpRequest(socket) { + let request = request + request.address = try? socket.peername() + let (params, handler) = self.dispatch(request) + request.params = params + let response = handler(request) + var keepConnection = parser.supportsKeepAlive(request.headers) + do { + if self.operating { + keepConnection = try self.respond(socket, response: response, keepAlive: keepConnection) + } + } catch { + print("Failed to send response: \(error)") + } + if let session = response.socketSession() { + delegate?.socketConnectionReceived(socket) + session(socket) + break + } + if !keepConnection { break } + } + socket.close() + } + + private struct InnerWriteContext: HttpResponseBodyWriter { + + let socket: Socket + + func write(_ file: String.File) throws { + try socket.writeFile(file) + } + + func write(_ data: [UInt8]) throws { + try write(ArraySlice(data)) + } + + func write(_ data: ArraySlice) throws { + try socket.writeUInt8(data) + } + + func write(_ data: NSData) throws { + try socket.writeData(data) + } + + func write(_ data: Data) throws { + try socket.writeData(data) + } + } + + private func respond(_ socket: Socket, response: HttpResponse, keepAlive: Bool) throws -> Bool { + guard self.operating else { return false } + + // Some web-socket clients (like Jetfire) expects to have header section in a single packet. + // We can't promise that but make sure we invoke "write" only once for response header section. + + var responseHeader = String() + + responseHeader.append("HTTP/1.1 \(response.statusCode) \(response.reasonPhrase)\r\n") + + let content = response.content() + + if content.length >= 0 { + responseHeader.append("Content-Length: \(content.length)\r\n") + } + + if keepAlive && content.length != -1 { + responseHeader.append("Connection: keep-alive\r\n") + } + + for (name, value) in response.headers() { + responseHeader.append("\(name): \(value)\r\n") + } + + responseHeader.append("\r\n") + + try socket.writeUTF8(responseHeader) + + if let writeClosure = content.write { + let context = InnerWriteContext(socket: socket) + try writeClosure(context) + } + + return keepAlive && content.length != -1 + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/MimeTypes.swift b/TestTools/StreamChatTestMockServer/Swifter/MimeTypes.swift new file mode 100644 index 00000000000..204a262c792 --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/MimeTypes.swift @@ -0,0 +1,141 @@ +// +// MimeTypes.swift +// Swifter +// +// Created by Daniel Große on 16.02.18. +// + +import Foundation + +internal let DEFAULT_MIME_TYPE = "application/octet-stream" + +internal let mimeTypes = [ + "html": "text/html", + "htm": "text/html", + "shtml": "text/html", + "css": "text/css", + "xml": "text/xml", + "gif": "image/gif", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "js": "application/javascript", + "atom": "application/atom+xml", + "rss": "application/rss+xml", + "mml": "text/mathml", + "txt": "text/plain", + "jad": "text/vnd.sun.j2me.app-descriptor", + "wml": "text/vnd.wap.wml", + "htc": "text/x-component", + "png": "image/png", + "tif": "image/tiff", + "tiff": "image/tiff", + "wbmp": "image/vnd.wap.wbmp", + "ico": "image/x-icon", + "jng": "image/x-jng", + "bmp": "image/x-ms-bmp", + "svg": "image/svg+xml", + "svgz": "image/svg+xml", + "webp": "image/webp", + "woff": "application/font-woff", + "jar": "application/java-archive", + "war": "application/java-archive", + "ear": "application/java-archive", + "json": "application/json", + "hqx": "application/mac-binhex40", + "doc": "application/msword", + "pdf": "application/pdf", + "ps": "application/postscript", + "eps": "application/postscript", + "ai": "application/postscript", + "rtf": "application/rtf", + "m3u8": "application/vnd.apple.mpegurl", + "xls": "application/vnd.ms-excel", + "eot": "application/vnd.ms-fontobject", + "ppt": "application/vnd.ms-powerpoint", + "wmlc": "application/vnd.wap.wmlc", + "kml": "application/vnd.google-earth.kml+xml", + "kmz": "application/vnd.google-earth.kmz", + "7z": "application/x-7z-compressed", + "cco": "application/x-cocoa", + "jardiff": "application/x-java-archive-diff", + "jnlp": "application/x-java-jnlp-file", + "run": "application/x-makeself", + "pl": "application/x-perl", + "pm": "application/x-perl", + "prc": "application/x-pilot", + "pdb": "application/x-pilot", + "rar": "application/x-rar-compressed", + "rpm": "application/x-redhat-package-manager", + "sea": "application/x-sea", + "swf": "application/x-shockwave-flash", + "sit": "application/x-stuffit", + "tcl": "application/x-tcl", + "tk": "application/x-tcl", + "der": "application/x-x509-ca-cert", + "pem": "application/x-x509-ca-cert", + "crt": "application/x-x509-ca-cert", + "xpi": "application/x-xpinstall", + "xhtml": "application/xhtml+xml", + "xspf": "application/xspf+xml", + "zip": "application/zip", + "bin": "application/octet-stream", + "exe": "application/octet-stream", + "dll": "application/octet-stream", + "deb": "application/octet-stream", + "dmg": "application/octet-stream", + "iso": "application/octet-stream", + "img": "application/octet-stream", + "msi": "application/octet-stream", + "msp": "application/octet-stream", + "msm": "application/octet-stream", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "mid": "audio/midi", + "midi": "audio/midi", + "kar": "audio/midi", + "mp3": "audio/mpeg", + "ogg": "audio/ogg", + "m4a": "audio/x-m4a", + "ra": "audio/x-realaudio", + "3gpp": "video/3gpp", + "3gp": "video/3gpp", + "ts": "video/mp2t", + "mp4": "video/mp4", + "mpeg": "video/mpeg", + "mpg": "video/mpeg", + "mov": "video/quicktime", + "webm": "video/webm", + "flv": "video/x-flv", + "m4v": "video/x-m4v", + "mng": "video/x-mng", + "asx": "video/x-ms-asf", + "asf": "video/x-ms-asf", + "wmv": "video/x-ms-wmv", + "avi": "video/x-msvideo" +] + +internal func matchMimeType(extens: String?) -> String { + if extens != nil && mimeTypes.contains(where: { $0.0 == extens!.lowercased() }) { + return mimeTypes[extens!.lowercased()]! + } + return DEFAULT_MIME_TYPE +} + +extension NSURL { + public func mimeType() -> String { + return matchMimeType(extens: self.pathExtension) + } +} + +extension NSString { + public func mimeType() -> String { + return matchMimeType(extens: self.pathExtension) + } +} + +extension String { + public func mimeType() -> String { + return (NSString(string: self)).mimeType() + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/Process.swift b/TestTools/StreamChatTestMockServer/Swifter/Process.swift new file mode 100644 index 00000000000..f6c214a83af --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/Process.swift @@ -0,0 +1,40 @@ +// +// Process +// Swifter +// +// Copyright (c) 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +public class Process { + + public static var pid: Int { + return Int(getpid()) + } + + public static var tid: UInt64 { + #if os(Linux) + return UInt64(pthread_self()) + #else + var tid: __uint64_t = 0 + pthread_threadid_np(nil, &tid) + return UInt64(tid) + #endif + } + + private static var signalsWatchers = [(Int32) -> Void]() + private static var signalsObserved = false + + public static func watchSignals(_ callback: @escaping (Int32) -> Void) { + if !signalsObserved { + [SIGTERM, SIGHUP, SIGSTOP, SIGINT].forEach { item in + signal(item) { signum in + Process.signalsWatchers.forEach { $0(signum) } + } + } + signalsObserved = true + } + signalsWatchers.append(callback) + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/Scopes.swift b/TestTools/StreamChatTestMockServer/Swifter/Scopes.swift new file mode 100644 index 00000000000..731920a7aeb --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/Scopes.swift @@ -0,0 +1,887 @@ +// +// HttpHandlers+Scopes.swift +// Swifter +// +// Copyright © 2014-2016 Damian Kołakowski. All rights reserved. +// + +// swiftlint:disable file_length +import Foundation + +public func scopes(_ scope: @escaping Closure) -> ((HttpRequest) -> HttpResponse) { + return { _ in + scopesBuffer[Process.tid] = "" + scope() + return .raw(200, "OK", ["Content-Type": "text/html"], { + try? $0.write([UInt8](("" + (scopesBuffer[Process.tid] ?? "")).utf8)) + }) + } +} + +public typealias Closure = () -> Void + +public var idd: String? +public var dir: String? +public var rel: String? +public var rev: String? +public var alt: String? +public var forr: String? +public var src: String? +public var type: String? +public var href: String? +public var text: String? +public var abbr: String? +public var size: String? +public var face: String? +public var char: String? +public var cite: String? +public var span: String? +public var data: String? +public var axis: String? +public var Name: String? +public var name: String? +public var code: String? +public var link: String? +public var lang: String? +public var cols: String? +public var rows: String? +public var ismap: String? +public var shape: String? +public var style: String? +public var alink: String? +public var width: String? +public var rules: String? +public var align: String? +public var frame: String? +public var vlink: String? +public var deferr: String? +public var color: String? +public var media: String? +public var title: String? +public var scope: String? +public var classs: String? +public var manifest: String? +public var value: String? +public var clear: String? +public var start: String? +public var label: String? +public var action: String? +public var height: String? +public var method: String? +public var acceptt: String? +public var object: String? +public var scheme: String? +public var coords: String? +public var usemap: String? +public var onblur: String? +public var nohref: String? +public var nowrap: String? +public var hspace: String? +public var border: String? +public var valign: String? +public var vspace: String? +public var onload: String? +public var target: String? +public var prompt: String? +public var onfocus: String? +public var enctype: String? +public var onclick: String? +public var ontouchstart: String? +public var onkeyup: String? +public var profile: String? +public var version: String? +public var onreset: String? +public var charset: String? +public var standby: String? +public var colspan: String? +public var charoff: String? +public var classid: String? +public var compact: String? +public var declare: String? +public var rowspan: String? +public var checked: String? +public var archive: String? +public var bgcolor: String? +public var content: String? +public var noshade: String? +public var summary: String? +public var headers: String? +public var onselect: String? +public var readonly: String? +public var tabindex: String? +public var onchange: String? +public var noresize: String? +public var disabled: String? +public var longdesc: String? +public var codebase: String? +public var language: String? +public var datetime: String? +public var selected: String? +public var hreflang: String? +public var onsubmit: String? +public var multiple: String? +public var onunload: String? +public var codetype: String? +public var scrolling: String? +public var onkeydown: String? +public var maxlength: String? +public var valuetype: String? +public var accesskey: String? +public var onmouseup: String? +public var autofocus: String? +public var onkeypress: String? +public var ondblclick: String? +public var onmouseout: String? +public var httpEquiv: String? +public var dataText: String? +public var background: String? +public var onmousemove: String? +public var onmouseover: String? +public var cellpadding: String? +public var onmousedown: String? +public var frameborder: String? +public var marginwidth: String? +public var cellspacing: String? +public var placeholder: String? +public var marginheight: String? +public var acceptCharset: String? + +public var inner: String? + +public func a(_ closure: Closure) { element("a", closure) } +public func b(_ closure: Closure) { element("b", closure) } +public func i(_ closure: Closure) { element("i", closure) } +public func p(_ closure: Closure) { element("p", closure) } +public func q(_ closure: Closure) { element("q", closure) } +public func s(_ closure: Closure) { element("s", closure) } +public func u(_ closure: Closure) { element("u", closure) } + +public func br(_ closure: Closure) { element("br", closure) } +public func dd(_ closure: Closure) { element("dd", closure) } +public func dl(_ closure: Closure) { element("dl", closure) } +public func dt(_ closure: Closure) { element("dt", closure) } +public func em(_ closure: Closure) { element("em", closure) } +public func hr(_ closure: Closure) { element("hr", closure) } +public func li(_ closure: Closure) { element("li", closure) } +public func ol(_ closure: Closure) { element("ol", closure) } +public func rp(_ closure: Closure) { element("rp", closure) } +public func rt(_ closure: Closure) { element("rt", closure) } +public func td(_ closure: Closure) { element("td", closure) } +public func th(_ closure: Closure) { element("th", closure) } +public func tr(_ closure: Closure) { element("tr", closure) } +public func tt(_ closure: Closure) { element("tt", closure) } +public func ul(_ closure: Closure) { element("ul", closure) } + +public func ul(_ collection: T, _ closure: @escaping (T.Iterator.Element) -> Void) { + element("ul", { + for item in collection { + closure(item) + } + }) +} + +public func h1(_ closure: Closure) { element("h1", closure) } +public func h2(_ closure: Closure) { element("h2", closure) } +public func h3(_ closure: Closure) { element("h3", closure) } +public func h4(_ closure: Closure) { element("h4", closure) } +public func h5(_ closure: Closure) { element("h5", closure) } +public func h6(_ closure: Closure) { element("h6", closure) } + +public func bdi(_ closure: Closure) { element("bdi", closure) } +public func bdo(_ closure: Closure) { element("bdo", closure) } +public func big(_ closure: Closure) { element("big", closure) } +public func col(_ closure: Closure) { element("col", closure) } +public func del(_ closure: Closure) { element("del", closure) } +public func dfn(_ closure: Closure) { element("dfn", closure) } +public func dir(_ closure: Closure) { element("dir", closure) } +public func div(_ closure: Closure) { element("div", closure) } +public func img(_ closure: Closure) { element("img", closure) } +public func ins(_ closure: Closure) { element("ins", closure) } +public func kbd(_ closure: Closure) { element("kbd", closure) } +public func map(_ closure: Closure) { element("map", closure) } +public func nav(_ closure: Closure) { element("nav", closure) } +public func pre(_ closure: Closure) { element("pre", closure) } +public func rtc(_ closure: Closure) { element("rtc", closure) } +public func sub(_ closure: Closure) { element("sub", closure) } +public func sup(_ closure: Closure) { element("sup", closure) } + +public func varr(_ closure: Closure) { element("var", closure) } +public func wbr(_ closure: Closure) { element("wbr", closure) } +public func xmp(_ closure: Closure) { element("xmp", closure) } + +public func abbr(_ closure: Closure) { element("abbr", closure) } +public func area(_ closure: Closure) { element("area", closure) } +public func base(_ closure: Closure) { element("base", closure) } +public func body(_ closure: Closure) { element("body", closure) } +public func cite(_ closure: Closure) { element("cite", closure) } +public func code(_ closure: Closure) { element("code", closure) } +public func data(_ closure: Closure) { element("data", closure) } +public func font(_ closure: Closure) { element("font", closure) } +public func form(_ closure: Closure) { element("form", closure) } +public func head(_ closure: Closure) { element("head", closure) } +public func html(_ closure: Closure) { element("html", closure) } +public func link(_ closure: Closure) { element("link", closure) } +public func main(_ closure: Closure) { element("main", closure) } +public func mark(_ closure: Closure) { element("mark", closure) } +public func menu(_ closure: Closure) { element("menu", closure) } +public func meta(_ closure: Closure) { element("meta", closure) } +public func nobr(_ closure: Closure) { element("nobr", closure) } +public func ruby(_ closure: Closure) { element("ruby", closure) } +public func samp(_ closure: Closure) { element("samp", closure) } +public func span(_ closure: Closure) { element("span", closure) } +public func time(_ closure: Closure) { element("time", closure) } + +public func aside(_ closure: Closure) { element("aside", closure) } +public func audio(_ closure: Closure) { element("audio", closure) } +public func blink(_ closure: Closure) { element("blink", closure) } +public func embed(_ closure: Closure) { element("embed", closure) } +public func frame(_ closure: Closure) { element("frame", closure) } +public func image(_ closure: Closure) { element("image", closure) } +public func input(_ closure: Closure) { element("input", closure) } +public func label(_ closure: Closure) { element("label", closure) } +public func meter(_ closure: Closure) { element("meter", closure) } +public func param(_ closure: Closure) { element("param", closure) } +public func small(_ closure: Closure) { element("small", closure) } +public func style(_ closure: Closure) { element("style", closure) } +public func table(_ closure: Closure) { element("table", closure) } + +public func table(_ collection: T, closure: @escaping (T.Iterator.Element) -> Void) { + element("table", { + for item in collection { + closure(item) + } + }) +} + +public func tbody(_ closure: Closure) { element("tbody", closure) } + +public func tbody(_ collection: T, closure: @escaping (T.Iterator.Element) -> Void) { + element("tbody", { + for item in collection { + closure(item) + } + }) +} + +public func tfoot(_ closure: Closure) { element("tfoot", closure) } +public func thead(_ closure: Closure) { element("thead", closure) } +public func title(_ closure: Closure) { element("title", closure) } +public func track(_ closure: Closure) { element("track", closure) } +public func video(_ closure: Closure) { element("video", closure) } + +public func applet(_ closure: Closure) { element("applet", closure) } +public func button(_ closure: Closure) { element("button", closure) } +public func canvas(_ closure: Closure) { element("canvas", closure) } +public func center(_ closure: Closure) { element("center", closure) } +public func dialog(_ closure: Closure) { element("dialog", closure) } +public func figure(_ closure: Closure) { element("figure", closure) } +public func footer(_ closure: Closure) { element("footer", closure) } +public func header(_ closure: Closure) { element("header", closure) } +public func hgroup(_ closure: Closure) { element("hgroup", closure) } +public func iframe(_ closure: Closure) { element("iframe", closure) } +public func keygen(_ closure: Closure) { element("keygen", closure) } +public func legend(_ closure: Closure) { element("legend", closure) } +public func object(_ closure: Closure) { element("object", closure) } +public func option(_ closure: Closure) { element("option", closure) } +public func output(_ closure: Closure) { element("output", closure) } +public func script(_ closure: Closure) { element("script", closure) } +public func select(_ closure: Closure) { element("select", closure) } +public func shadow(_ closure: Closure) { element("shadow", closure) } +public func source(_ closure: Closure) { element("source", closure) } +public func spacer(_ closure: Closure) { element("spacer", closure) } +public func strike(_ closure: Closure) { element("strike", closure) } +public func strong(_ closure: Closure) { element("strong", closure) } + +public func acronym(_ closure: Closure) { element("acronym", closure) } +public func address(_ closure: Closure) { element("address", closure) } +public func article(_ closure: Closure) { element("article", closure) } +public func bgsound(_ closure: Closure) { element("bgsound", closure) } +public func caption(_ closure: Closure) { element("caption", closure) } +public func command(_ closure: Closure) { element("command", closure) } +public func content(_ closure: Closure) { element("content", closure) } +public func details(_ closure: Closure) { element("details", closure) } +public func elementt(_ closure: Closure) { element("element", closure) } +public func isindex(_ closure: Closure) { element("isindex", closure) } +public func listing(_ closure: Closure) { element("listing", closure) } +public func marquee(_ closure: Closure) { element("marquee", closure) } +public func noembed(_ closure: Closure) { element("noembed", closure) } +public func picture(_ closure: Closure) { element("picture", closure) } +public func section(_ closure: Closure) { element("section", closure) } +public func summary(_ closure: Closure) { element("summary", closure) } + +public func basefont(_ closure: Closure) { element("basefont", closure) } +public func colgroup(_ closure: Closure) { element("colgroup", closure) } +public func datalist(_ closure: Closure) { element("datalist", closure) } +public func fieldset(_ closure: Closure) { element("fieldset", closure) } +public func frameset(_ closure: Closure) { element("frameset", closure) } +public func menuitem(_ closure: Closure) { element("menuitem", closure) } +public func multicol(_ closure: Closure) { element("multicol", closure) } +public func noframes(_ closure: Closure) { element("noframes", closure) } +public func noscript(_ closure: Closure) { element("noscript", closure) } +public func optgroup(_ closure: Closure) { element("optgroup", closure) } +public func progress(_ closure: Closure) { element("progress", closure) } +public func template(_ closure: Closure) { element("template", closure) } +public func textarea(_ closure: Closure) { element("textarea", closure) } + +public func plaintext(_ closure: Closure) { element("plaintext", closure) } +public func javascript(_ closure: Closure) { element("script", ["type": "text/javascript"], closure) } +public func blockquote(_ closure: Closure) { element("blockquote", closure) } +public func figcaption(_ closure: Closure) { element("figcaption", closure) } + +public func stylesheet(_ closure: Closure) { element("link", ["rel": "stylesheet", "type": "text/css"], closure) } + +public func element(_ node: String, _ closure: Closure) { evaluate(node, [:], closure) } +public func element(_ node: String, _ attrs: [String: String?] = [:], _ closure: Closure) { evaluate(node, attrs, closure) } + +var scopesBuffer = [UInt64: String]() + +// swiftlint:disable cyclomatic_complexity function_body_length +private func evaluate(_ node: String, _ attrs: [String: String?] = [:], _ closure: Closure) { + + // Push the attributes. + + let stackid = idd + let stackdir = dir + let stackrel = rel + let stackrev = rev + let stackalt = alt + let stackfor = forr + let stacksrc = src + let stacktype = type + let stackhref = href + let stacktext = text + let stackabbr = abbr + let stacksize = size + let stackface = face + let stackchar = char + let stackcite = cite + let stackspan = span + let stackdata = data + let stackaxis = axis + let stackName = Name + let stackname = name + let stackcode = code + let stacklink = link + let stacklang = lang + let stackcols = cols + let stackrows = rows + let stackismap = ismap + let stackshape = shape + let stackstyle = style + let stackalink = alink + let stackwidth = width + let stackrules = rules + let stackalign = align + let stackframe = frame + let stackvlink = vlink + let stackdefer = deferr + let stackcolor = color + let stackmedia = media + let stacktitle = title + let stackscope = scope + let stackclass = classs + let stackmanifest = manifest + let stackvalue = value + let stackclear = clear + let stackstart = start + let stacklabel = label + let stackaction = action + let stackheight = height + let stackmethod = method + let stackaccept = acceptt + let stackobject = object + let stackscheme = scheme + let stackcoords = coords + let stackusemap = usemap + let stackonblur = onblur + let stacknohref = nohref + let stacknowrap = nowrap + let stackhspace = hspace + let stackborder = border + let stackvalign = valign + let stackvspace = vspace + let stackonload = onload + let stacktarget = target + let stackprompt = prompt + let stackonfocus = onfocus + let stackenctype = enctype + let stackonclick = onclick + let stackontouchstart = ontouchstart + let stackonkeyup = onkeyup + let stackprofile = profile + let stackversion = version + let stackonreset = onreset + let stackcharset = charset + let stackstandby = standby + let stackcolspan = colspan + let stackcharoff = charoff + let stackclassid = classid + let stackcompact = compact + let stackdeclare = declare + let stackrowspan = rowspan + let stackchecked = checked + let stackarchive = archive + let stackbgcolor = bgcolor + let stackcontent = content + let stacknoshade = noshade + let stacksummary = summary + let stackheaders = headers + let stackonselect = onselect + let stackreadonly = readonly + let stacktabindex = tabindex + let stackonchange = onchange + let stacknoresize = noresize + let stackdisabled = disabled + let stacklongdesc = longdesc + let stackcodebase = codebase + let stacklanguage = language + let stackdatetime = datetime + let stackselected = selected + let stackhreflang = hreflang + let stackonsubmit = onsubmit + let stackmultiple = multiple + let stackonunload = onunload + let stackcodetype = codetype + let stackscrolling = scrolling + let stackonkeydown = onkeydown + let stackmaxlength = maxlength + let stackvaluetype = valuetype + let stackaccesskey = accesskey + let stackonmouseup = onmouseup + let stackonkeypress = onkeypress + let stackondblclick = ondblclick + let stackonmouseout = onmouseout + let stackhttpEquiv = httpEquiv + let stackdataText = dataText + let stackbackground = background + let stackonmousemove = onmousemove + let stackonmouseover = onmouseover + let stackcellpadding = cellpadding + let stackonmousedown = onmousedown + let stackframeborder = frameborder + let stackmarginwidth = marginwidth + let stackcellspacing = cellspacing + let stackplaceholder = placeholder + let stackmarginheight = marginheight + let stackacceptCharset = acceptCharset + let stackinner = inner + + // Reset the values before a nested scope evalutation. + + idd = nil + dir = nil + rel = nil + rev = nil + alt = nil + forr = nil + src = nil + type = nil + href = nil + text = nil + abbr = nil + size = nil + face = nil + char = nil + cite = nil + span = nil + data = nil + axis = nil + Name = nil + name = nil + code = nil + link = nil + lang = nil + cols = nil + rows = nil + ismap = nil + shape = nil + style = nil + alink = nil + width = nil + rules = nil + align = nil + frame = nil + vlink = nil + deferr = nil + color = nil + media = nil + title = nil + scope = nil + classs = nil + manifest = nil + value = nil + clear = nil + start = nil + label = nil + action = nil + height = nil + method = nil + acceptt = nil + object = nil + scheme = nil + coords = nil + usemap = nil + onblur = nil + nohref = nil + nowrap = nil + hspace = nil + border = nil + valign = nil + vspace = nil + onload = nil + target = nil + prompt = nil + onfocus = nil + enctype = nil + onclick = nil + ontouchstart = nil + onkeyup = nil + profile = nil + version = nil + onreset = nil + charset = nil + standby = nil + colspan = nil + charoff = nil + classid = nil + compact = nil + declare = nil + rowspan = nil + checked = nil + archive = nil + bgcolor = nil + content = nil + noshade = nil + summary = nil + headers = nil + onselect = nil + readonly = nil + tabindex = nil + onchange = nil + noresize = nil + disabled = nil + longdesc = nil + codebase = nil + language = nil + datetime = nil + selected = nil + hreflang = nil + onsubmit = nil + multiple = nil + onunload = nil + codetype = nil + scrolling = nil + onkeydown = nil + maxlength = nil + valuetype = nil + accesskey = nil + onmouseup = nil + onkeypress = nil + ondblclick = nil + onmouseout = nil + httpEquiv = nil + dataText = nil + background = nil + onmousemove = nil + onmouseover = nil + cellpadding = nil + onmousedown = nil + frameborder = nil + placeholder = nil + marginwidth = nil + cellspacing = nil + marginheight = nil + acceptCharset = nil + inner = nil + + scopesBuffer[Process.tid] = (scopesBuffer[Process.tid] ?? "") + "<" + node + + // Save the current output before the nested scope evalutation. + + var output = scopesBuffer[Process.tid] ?? "" + + // Clear the output buffer for the evalutation. + + scopesBuffer[Process.tid] = "" + + // Evaluate the nested scope. + + closure() + + // Render attributes set by the evalutation. + + var mergedAttributes = [String: String?]() + + if let idd = idd { mergedAttributes["id"] = idd } + if let dir = dir { mergedAttributes["dir"] = dir } + if let rel = rel { mergedAttributes["rel"] = rel } + if let rev = rev { mergedAttributes["rev"] = rev } + if let alt = alt { mergedAttributes["alt"] = alt } + if let forr = forr { mergedAttributes["for"] = forr } + if let src = src { mergedAttributes["src"] = src } + if let type = type { mergedAttributes["type"] = type } + if let href = href { mergedAttributes["href"] = href } + if let text = text { mergedAttributes["text"] = text } + if let abbr = abbr { mergedAttributes["abbr"] = abbr } + if let size = size { mergedAttributes["size"] = size } + if let face = face { mergedAttributes["face"] = face } + if let char = char { mergedAttributes["char"] = char } + if let cite = cite { mergedAttributes["cite"] = cite } + if let span = span { mergedAttributes["span"] = span } + if let data = data { mergedAttributes["data"] = data } + if let axis = axis { mergedAttributes["axis"] = axis } + if let Name = Name { mergedAttributes["Name"] = Name } + if let name = name { mergedAttributes["name"] = name } + if let code = code { mergedAttributes["code"] = code } + if let link = link { mergedAttributes["link"] = link } + if let lang = lang { mergedAttributes["lang"] = lang } + if let cols = cols { mergedAttributes["cols"] = cols } + if let rows = rows { mergedAttributes["rows"] = rows } + if let ismap = ismap { mergedAttributes["ismap"] = ismap } + if let shape = shape { mergedAttributes["shape"] = shape } + if let style = style { mergedAttributes["style"] = style } + if let alink = alink { mergedAttributes["alink"] = alink } + if let width = width { mergedAttributes["width"] = width } + if let rules = rules { mergedAttributes["rules"] = rules } + if let align = align { mergedAttributes["align"] = align } + if let frame = frame { mergedAttributes["frame"] = frame } + if let vlink = vlink { mergedAttributes["vlink"] = vlink } + if let deferr = deferr { mergedAttributes["defer"] = deferr } + if let color = color { mergedAttributes["color"] = color } + if let media = media { mergedAttributes["media"] = media } + if let title = title { mergedAttributes["title"] = title } + if let scope = scope { mergedAttributes["scope"] = scope } + if let classs = classs { mergedAttributes["class"] = classs } + if let manifest = manifest { mergedAttributes["manifest"] = manifest } + if let value = value { mergedAttributes["value"] = value } + if let clear = clear { mergedAttributes["clear"] = clear } + if let start = start { mergedAttributes["start"] = start } + if let label = label { mergedAttributes["label"] = label } + if let action = action { mergedAttributes["action"] = action } + if let height = height { mergedAttributes["height"] = height } + if let method = method { mergedAttributes["method"] = method } + if let acceptt = acceptt { mergedAttributes["accept"] = acceptt } + if let object = object { mergedAttributes["object"] = object } + if let scheme = scheme { mergedAttributes["scheme"] = scheme } + if let coords = coords { mergedAttributes["coords"] = coords } + if let usemap = usemap { mergedAttributes["usemap"] = usemap } + if let onblur = onblur { mergedAttributes["onblur"] = onblur } + if let nohref = nohref { mergedAttributes["nohref"] = nohref } + if let nowrap = nowrap { mergedAttributes["nowrap"] = nowrap } + if let hspace = hspace { mergedAttributes["hspace"] = hspace } + if let border = border { mergedAttributes["border"] = border } + if let valign = valign { mergedAttributes["valign"] = valign } + if let vspace = vspace { mergedAttributes["vspace"] = vspace } + if let onload = onload { mergedAttributes["onload"] = onload } + if let target = target { mergedAttributes["target"] = target } + if let prompt = prompt { mergedAttributes["prompt"] = prompt } + if let onfocus = onfocus { mergedAttributes["onfocus"] = onfocus } + if let enctype = enctype { mergedAttributes["enctype"] = enctype } + if let onclick = onclick { mergedAttributes["onclick"] = onclick } + if let ontouchstart = ontouchstart { mergedAttributes["ontouchstart"] = ontouchstart } + if let onkeyup = onkeyup { mergedAttributes["onkeyup"] = onkeyup } + if let profile = profile { mergedAttributes["profile"] = profile } + if let version = version { mergedAttributes["version"] = version } + if let onreset = onreset { mergedAttributes["onreset"] = onreset } + if let charset = charset { mergedAttributes["charset"] = charset } + if let standby = standby { mergedAttributes["standby"] = standby } + if let colspan = colspan { mergedAttributes["colspan"] = colspan } + if let charoff = charoff { mergedAttributes["charoff"] = charoff } + if let classid = classid { mergedAttributes["classid"] = classid } + if let compact = compact { mergedAttributes["compact"] = compact } + if let declare = declare { mergedAttributes["declare"] = declare } + if let rowspan = rowspan { mergedAttributes["rowspan"] = rowspan } + if let checked = checked { mergedAttributes["checked"] = checked } + if let archive = archive { mergedAttributes["archive"] = archive } + if let bgcolor = bgcolor { mergedAttributes["bgcolor"] = bgcolor } + if let content = content { mergedAttributes["content"] = content } + if let noshade = noshade { mergedAttributes["noshade"] = noshade } + if let summary = summary { mergedAttributes["summary"] = summary } + if let headers = headers { mergedAttributes["headers"] = headers } + if let onselect = onselect { mergedAttributes["onselect"] = onselect } + if let readonly = readonly { mergedAttributes["readonly"] = readonly } + if let tabindex = tabindex { mergedAttributes["tabindex"] = tabindex } + if let onchange = onchange { mergedAttributes["onchange"] = onchange } + if let noresize = noresize { mergedAttributes["noresize"] = noresize } + if let disabled = disabled { mergedAttributes["disabled"] = disabled } + if let longdesc = longdesc { mergedAttributes["longdesc"] = longdesc } + if let codebase = codebase { mergedAttributes["codebase"] = codebase } + if let language = language { mergedAttributes["language"] = language } + if let datetime = datetime { mergedAttributes["datetime"] = datetime } + if let selected = selected { mergedAttributes["selected"] = selected } + if let hreflang = hreflang { mergedAttributes["hreflang"] = hreflang } + if let onsubmit = onsubmit { mergedAttributes["onsubmit"] = onsubmit } + if let multiple = multiple { mergedAttributes["multiple"] = multiple } + if let onunload = onunload { mergedAttributes["onunload"] = onunload } + if let codetype = codetype { mergedAttributes["codetype"] = codetype } + if let scrolling = scrolling { mergedAttributes["scrolling"] = scrolling } + if let onkeydown = onkeydown { mergedAttributes["onkeydown"] = onkeydown } + if let maxlength = maxlength { mergedAttributes["maxlength"] = maxlength } + if let valuetype = valuetype { mergedAttributes["valuetype"] = valuetype } + if let accesskey = accesskey { mergedAttributes["accesskey"] = accesskey } + if let onmouseup = onmouseup { mergedAttributes["onmouseup"] = onmouseup } + if let onkeypress = onkeypress { mergedAttributes["onkeypress"] = onkeypress } + if let ondblclick = ondblclick { mergedAttributes["ondblclick"] = ondblclick } + if let onmouseout = onmouseout { mergedAttributes["onmouseout"] = onmouseout } + if let httpEquiv = httpEquiv { mergedAttributes["http-equiv"] = httpEquiv } + if let dataText = dataText { mergedAttributes["data-text"] = dataText } + if let background = background { mergedAttributes["background"] = background } + if let onmousemove = onmousemove { mergedAttributes["onmousemove"] = onmousemove } + if let onmouseover = onmouseover { mergedAttributes["onmouseover"] = onmouseover } + if let cellpadding = cellpadding { mergedAttributes["cellpadding"] = cellpadding } + if let onmousedown = onmousedown { mergedAttributes["onmousedown"] = onmousedown } + if let frameborder = frameborder { mergedAttributes["frameborder"] = frameborder } + if let marginwidth = marginwidth { mergedAttributes["marginwidth"] = marginwidth } + if let cellspacing = cellspacing { mergedAttributes["cellspacing"] = cellspacing } + if let placeholder = placeholder { mergedAttributes["placeholder"] = placeholder } + if let marginheight = marginheight { mergedAttributes["marginheight"] = marginheight } + if let acceptCharset = acceptCharset { mergedAttributes["accept-charset"] = acceptCharset } + + for item in attrs.enumerated() { + mergedAttributes.updateValue(item.element.1, forKey: item.element.0) + } + + output += mergedAttributes.reduce("") { result, item in + if let value = item.value { + return result + " \(item.key)=\"\(value)\"" + } else { + return result + } + } + + if let inner = inner { + scopesBuffer[Process.tid] = output + ">" + (inner) + "" + } else { + let current = scopesBuffer[Process.tid] ?? "" + scopesBuffer[Process.tid] = output + ">" + current + "" + } + + // Pop the attributes. + + idd = stackid + dir = stackdir + rel = stackrel + rev = stackrev + alt = stackalt + forr = stackfor + src = stacksrc + type = stacktype + href = stackhref + text = stacktext + abbr = stackabbr + size = stacksize + face = stackface + char = stackchar + cite = stackcite + span = stackspan + data = stackdata + axis = stackaxis + Name = stackName + name = stackname + code = stackcode + link = stacklink + lang = stacklang + cols = stackcols + rows = stackrows + ismap = stackismap + shape = stackshape + style = stackstyle + alink = stackalink + width = stackwidth + rules = stackrules + align = stackalign + frame = stackframe + vlink = stackvlink + deferr = stackdefer + color = stackcolor + media = stackmedia + title = stacktitle + scope = stackscope + classs = stackclass + manifest = stackmanifest + value = stackvalue + clear = stackclear + start = stackstart + label = stacklabel + action = stackaction + height = stackheight + method = stackmethod + acceptt = stackaccept + object = stackobject + scheme = stackscheme + coords = stackcoords + usemap = stackusemap + onblur = stackonblur + nohref = stacknohref + nowrap = stacknowrap + hspace = stackhspace + border = stackborder + valign = stackvalign + vspace = stackvspace + onload = stackonload + target = stacktarget + prompt = stackprompt + onfocus = stackonfocus + enctype = stackenctype + onclick = stackonclick + ontouchstart = stackontouchstart + onkeyup = stackonkeyup + profile = stackprofile + version = stackversion + onreset = stackonreset + charset = stackcharset + standby = stackstandby + colspan = stackcolspan + charoff = stackcharoff + classid = stackclassid + compact = stackcompact + declare = stackdeclare + rowspan = stackrowspan + checked = stackchecked + archive = stackarchive + bgcolor = stackbgcolor + content = stackcontent + noshade = stacknoshade + summary = stacksummary + headers = stackheaders + onselect = stackonselect + readonly = stackreadonly + tabindex = stacktabindex + onchange = stackonchange + noresize = stacknoresize + disabled = stackdisabled + longdesc = stacklongdesc + codebase = stackcodebase + language = stacklanguage + datetime = stackdatetime + selected = stackselected + hreflang = stackhreflang + onsubmit = stackonsubmit + multiple = stackmultiple + onunload = stackonunload + codetype = stackcodetype + scrolling = stackscrolling + onkeydown = stackonkeydown + maxlength = stackmaxlength + valuetype = stackvaluetype + accesskey = stackaccesskey + onmouseup = stackonmouseup + onkeypress = stackonkeypress + ondblclick = stackondblclick + onmouseout = stackonmouseout + httpEquiv = stackhttpEquiv + dataText = stackdataText + background = stackbackground + onmousemove = stackonmousemove + onmouseover = stackonmouseover + cellpadding = stackcellpadding + onmousedown = stackonmousedown + frameborder = stackframeborder + placeholder = stackplaceholder + marginwidth = stackmarginwidth + cellspacing = stackcellspacing + marginheight = stackmarginheight + acceptCharset = stackacceptCharset + + inner = stackinner +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/Socket+File.swift b/TestTools/StreamChatTestMockServer/Swifter/Socket+File.swift new file mode 100644 index 00000000000..e0a9e82ad24 --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/Socket+File.swift @@ -0,0 +1,57 @@ +// +// Socket+File.swift +// Swifter +// +// Created by Damian Kolakowski on 13/07/16. +// + +import Foundation + +#if os(iOS) || os(tvOS) || os (Linux) +// swiftlint:disable type_name function_parameter_count + struct sf_hdtr { } + + private func sendfileImpl(_ source: UnsafeMutablePointer, _ target: Int32, _: off_t, _: UnsafeMutablePointer, _: UnsafeMutablePointer, _: Int32) -> Int32 { + var buffer = [UInt8](repeating: 0, count: 1024) + while true { + let readResult = fread(&buffer, 1, buffer.count, source) + guard readResult > 0 else { + return Int32(readResult) + } + var writeCounter = 0 + while writeCounter < readResult { + let writeResult = buffer.withUnsafeBytes { (ptr) -> Int in + let start = ptr.baseAddress! + writeCounter + let len = readResult - writeCounter + #if os(Linux) + return send(target, start, len, Int32(MSG_NOSIGNAL)) + #else + return write(target, start, len) + #endif + } + guard writeResult > 0 else { + return Int32(writeResult) + } + writeCounter += writeResult + } + } + } +#endif + +extension Socket { + + public func writeFile(_ file: String.File) throws { + var offset: off_t = 0 + var sf: sf_hdtr = sf_hdtr() + + #if os(iOS) || os(tvOS) || os (Linux) + let result = sendfileImpl(file.pointer, self.socketFileDescriptor, 0, &offset, &sf, 0) + #else + let result = sendfile(fileno(file.pointer), self.socketFileDescriptor, 0, &offset, &sf, 0) + #endif + + if result == -1 { + throw SocketError.writeFailed("sendfile: " + Errno.description()) + } + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/Socket+Server.swift b/TestTools/StreamChatTestMockServer/Swifter/Socket+Server.swift new file mode 100644 index 00000000000..11076a1e8b7 --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/Socket+Server.swift @@ -0,0 +1,116 @@ +// +// Socket+Server.swift +// Swifter +// +// Created by Damian Kolakowski on 13/07/16. +// + +import Foundation + +extension Socket { + + // swiftlint:disable function_body_length + /// - Parameters: + /// - listenAddress: String representation of the address the socket should accept + /// connections from. It should be in IPv4 format if forceIPv4 == true, + /// otherwise - in IPv6. + public class func tcpSocketForListen(_ port: in_port_t, _ forceIPv4: Bool = false, _ maxPendingConnection: Int32 = SOMAXCONN, _ listenAddress: String? = nil) throws -> Socket { + + #if os(Linux) + let socketFileDescriptor = socket(forceIPv4 ? AF_INET : AF_INET6, Int32(SOCK_STREAM.rawValue), 0) + #else + let socketFileDescriptor = socket(forceIPv4 ? AF_INET : AF_INET6, SOCK_STREAM, 0) + #endif + + if socketFileDescriptor == -1 { + throw SocketError.socketCreationFailed(Errno.description()) + } + + var value: Int32 = 1 + if setsockopt(socketFileDescriptor, SOL_SOCKET, SO_REUSEADDR, &value, socklen_t(MemoryLayout.size)) == -1 { + let details = Errno.description() + Socket.close(socketFileDescriptor) + throw SocketError.socketSettingReUseAddrFailed(details) + } + Socket.setNoSigPipe(socketFileDescriptor) + + var bindResult: Int32 = -1 + if forceIPv4 { + #if os(Linux) + var addr = sockaddr_in( + sin_family: sa_family_t(AF_INET), + sin_port: port.bigEndian, + sin_addr: in_addr(s_addr: in_addr_t(0)), + sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) + #else + var addr = sockaddr_in( + sin_len: UInt8(MemoryLayout.stride), + sin_family: UInt8(AF_INET), + sin_port: port.bigEndian, + sin_addr: in_addr(s_addr: in_addr_t(0)), + sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) + #endif + if let address = listenAddress { + if address.withCString({ cstring in inet_pton(AF_INET, cstring, &addr.sin_addr) }) == 1 { + // print("\(address) is converted to \(addr.sin_addr).") + } else { + // print("\(address) is not converted.") + } + } + bindResult = withUnsafePointer(to: &addr) { + bind(socketFileDescriptor, UnsafePointer(OpaquePointer($0)), socklen_t(MemoryLayout.size)) + } + } else { + #if os(Linux) + var addr = sockaddr_in6( + sin6_family: sa_family_t(AF_INET6), + sin6_port: port.bigEndian, + sin6_flowinfo: 0, + sin6_addr: in6addr_any, + sin6_scope_id: 0) + #else + var addr = sockaddr_in6( + sin6_len: UInt8(MemoryLayout.stride), + sin6_family: UInt8(AF_INET6), + sin6_port: port.bigEndian, + sin6_flowinfo: 0, + sin6_addr: in6addr_any, + sin6_scope_id: 0) + #endif + if let address = listenAddress { + if address.withCString({ cstring in inet_pton(AF_INET6, cstring, &addr.sin6_addr) }) == 1 { + //print("\(address) is converted to \(addr.sin6_addr).") + } else { + //print("\(address) is not converted.") + } + } + bindResult = withUnsafePointer(to: &addr) { + bind(socketFileDescriptor, UnsafePointer(OpaquePointer($0)), socklen_t(MemoryLayout.size)) + } + } + + if bindResult == -1 { + let details = Errno.description() + Socket.close(socketFileDescriptor) + throw SocketError.bindFailed(details) + } + + if listen(socketFileDescriptor, maxPendingConnection) == -1 { + let details = Errno.description() + Socket.close(socketFileDescriptor) + throw SocketError.listenFailed(details) + } + return Socket(socketFileDescriptor: socketFileDescriptor) + } + + public func acceptClientSocket() throws -> Socket { + var addr = sockaddr() + var len: socklen_t = 0 + let clientSocket = accept(self.socketFileDescriptor, &addr, &len) + if clientSocket == -1 { + throw SocketError.acceptFailed(Errno.description()) + } + Socket.setNoSigPipe(clientSocket) + return Socket(socketFileDescriptor: clientSocket) + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/Socket.swift b/TestTools/StreamChatTestMockServer/Swifter/Socket.swift new file mode 100644 index 00000000000..1a9887e9f6d --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/Socket.swift @@ -0,0 +1,236 @@ +// +// Socket.swift +// Swifter +// +// Copyright (c) 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +public enum SocketError: Error { + case socketCreationFailed(String) + case socketSettingReUseAddrFailed(String) + case bindFailed(String) + case listenFailed(String) + case writeFailed(String) + case getPeerNameFailed(String) + case convertingPeerNameFailed + case getNameInfoFailed(String) + case acceptFailed(String) + case recvFailed(String) + case getSockNameFailed(String) +} + +// swiftlint: disable identifier_name +open class Socket: Hashable, Equatable { + + let socketFileDescriptor: Int32 + private var shutdown = false + + public init(socketFileDescriptor: Int32) { + self.socketFileDescriptor = socketFileDescriptor + } + + deinit { + close() + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(self.socketFileDescriptor) + } + + public func close() { + if shutdown { + return + } + shutdown = true + Socket.close(self.socketFileDescriptor) + } + + public func port() throws -> in_port_t { + var addr = sockaddr_in() + return try withUnsafePointer(to: &addr) { pointer in + var len = socklen_t(MemoryLayout.size) + if getsockname(socketFileDescriptor, UnsafeMutablePointer(OpaquePointer(pointer)), &len) != 0 { + throw SocketError.getSockNameFailed(Errno.description()) + } + let sin_port = pointer.pointee.sin_port + #if os(Linux) + return ntohs(sin_port) + #else + return Int(OSHostByteOrder()) != OSLittleEndian ? sin_port.littleEndian : sin_port.bigEndian + #endif + } + } + + public func isIPv4() throws -> Bool { + var addr = sockaddr_in() + return try withUnsafePointer(to: &addr) { pointer in + var len = socklen_t(MemoryLayout.size) + if getsockname(socketFileDescriptor, UnsafeMutablePointer(OpaquePointer(pointer)), &len) != 0 { + throw SocketError.getSockNameFailed(Errno.description()) + } + return Int32(pointer.pointee.sin_family) == AF_INET + } + } + + public func writeUTF8(_ string: String) throws { + try writeUInt8(ArraySlice(string.utf8)) + } + + public func writeUInt8(_ data: [UInt8]) throws { + try writeUInt8(ArraySlice(data)) + } + + public func writeUInt8(_ data: ArraySlice) throws { + try data.withUnsafeBufferPointer { + try writeBuffer($0.baseAddress!, length: data.count) + } + } + + public func writeData(_ data: NSData) throws { + try writeBuffer(data.bytes, length: data.length) + } + + public func writeData(_ data: Data) throws { + #if compiler(>=5.0) + try data.withUnsafeBytes { (body: UnsafeRawBufferPointer) -> Void in + if let baseAddress = body.baseAddress, body.count > 0 { + let pointer = baseAddress.assumingMemoryBound(to: UInt8.self) + try self.writeBuffer(pointer, length: data.count) + } + } + #else + try data.withUnsafeBytes { (pointer: UnsafePointer) -> Void in + try self.writeBuffer(pointer, length: data.count) + } + #endif + } + + private func writeBuffer(_ pointer: UnsafeRawPointer, length: Int) throws { + var sent = 0 + while sent < length { + #if os(Linux) + let result = send(self.socketFileDescriptor, pointer + sent, Int(length - sent), Int32(MSG_NOSIGNAL)) + #else + let result = write(self.socketFileDescriptor, pointer + sent, Int(length - sent)) + #endif + if result <= 0 { + throw SocketError.writeFailed(Errno.description()) + } + sent += result + } + } + + /// Read a single byte off the socket. This method is optimized for reading + /// a single byte. For reading multiple bytes, use read(length:), which will + /// pre-allocate heap space and read directly into it. + /// + /// - Returns: A single byte + /// - Throws: SocketError.recvFailed if unable to read from the socket + open func read() throws -> UInt8 { + var byte: UInt8 = 0 + + #if os(Linux) + let count = Glibc.read(self.socketFileDescriptor as Int32, &byte, 1) + #else + let count = Darwin.read(self.socketFileDescriptor as Int32, &byte, 1) + #endif + + guard count > 0 else { + throw SocketError.recvFailed(Errno.description()) + } + return byte + } + + /// Read up to `length` bytes from this socket + /// + /// - Parameter length: The maximum bytes to read + /// - Returns: A buffer containing the bytes read + /// - Throws: SocketError.recvFailed if unable to read bytes from the socket + open func read(length: Int) throws -> [UInt8] { + return try [UInt8](unsafeUninitializedCapacity: length) { buffer, bytesRead in + bytesRead = try read(into: &buffer, length: length) + } + } + + static let kBufferLength = 1024 + + /// Read up to `length` bytes from this socket into an existing buffer + /// + /// - Parameter into: The buffer to read into (must be at least length bytes in size) + /// - Parameter length: The maximum bytes to read + /// - Returns: The number of bytes read + /// - Throws: SocketError.recvFailed if unable to read bytes from the socket + func read(into buffer: inout UnsafeMutableBufferPointer, length: Int) throws -> Int { + var offset = 0 + guard let baseAddress = buffer.baseAddress else { return 0 } + + while offset < length { + // Compute next read length in bytes. The bytes read is never more than kBufferLength at once. + let readLength = offset + Socket.kBufferLength < length ? Socket.kBufferLength : length - offset + + #if os(Linux) + let bytesRead = Glibc.read(self.socketFileDescriptor as Int32, baseAddress + offset, readLength) + #else + let bytesRead = Darwin.read(self.socketFileDescriptor as Int32, baseAddress + offset, readLength) + #endif + + guard bytesRead > 0 else { + throw SocketError.recvFailed(Errno.description()) + } + + offset += bytesRead + } + + return offset + } + + private static let CR: UInt8 = 13 + private static let NL: UInt8 = 10 + + public func readLine() throws -> String { + var characters: String = "" + var index: UInt8 = 0 + repeat { + index = try self.read() + if index > Socket.CR { characters.append(Character(UnicodeScalar(index))) } + } while index != Socket.NL + return characters + } + + public func peername() throws -> String { + var addr = sockaddr(), len: socklen_t = socklen_t(MemoryLayout.size) + if getpeername(self.socketFileDescriptor, &addr, &len) != 0 { + throw SocketError.getPeerNameFailed(Errno.description()) + } + var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + if getnameinfo(&addr, len, &hostBuffer, socklen_t(hostBuffer.count), nil, 0, NI_NUMERICHOST) != 0 { + throw SocketError.getNameInfoFailed(Errno.description()) + } + return String(cString: hostBuffer) + } + + public class func setNoSigPipe(_ socket: Int32) { + #if os(Linux) + // There is no SO_NOSIGPIPE in Linux (nor some other systems). You can instead use the MSG_NOSIGNAL flag when calling send(), + // or use signal(SIGPIPE, SIG_IGN) to make your entire application ignore SIGPIPE. + #else + // Prevents crashes when blocking calls are pending and the app is paused ( via Home button ). + var no_sig_pipe: Int32 = 1 + setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &no_sig_pipe, socklen_t(MemoryLayout.size)) + #endif + } + + public class func close(_ socket: Int32) { + #if os(Linux) + _ = Glibc.close(socket) + #else + _ = Darwin.close(socket) + #endif + } +} + +public func == (socket1: Socket, socket2: Socket) -> Bool { + return socket1.socketFileDescriptor == socket2.socketFileDescriptor +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/String+BASE64.swift b/TestTools/StreamChatTestMockServer/Swifter/String+BASE64.swift new file mode 100644 index 00000000000..15988452215 --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/String+BASE64.swift @@ -0,0 +1,15 @@ +// +// String+BASE64.swift +// Swifter +// +// Copyright © 2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +extension String { + + public static func toBase64(_ data: [UInt8]) -> String { + return Data(data).base64EncodedString() + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/String+File.swift b/TestTools/StreamChatTestMockServer/Swifter/String+File.swift new file mode 100644 index 00000000000..15fb7fb7458 --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/String+File.swift @@ -0,0 +1,147 @@ +// +// String+File.swift +// Swifter +// +// Copyright © 2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +extension String { + + public enum FileError: Error { + case error(Int32) + } + + public class File { + + let pointer: UnsafeMutablePointer + + public init(_ pointer: UnsafeMutablePointer) { + self.pointer = pointer + } + + public func close() { + fclose(pointer) + } + + public func seek(_ offset: Int) -> Bool { + return (fseek(pointer, offset, SEEK_SET) == 0) + } + + public func read(_ data: inout [UInt8]) throws -> Int { + if data.count <= 0 { + return data.count + } + let count = fread(&data, 1, data.count, self.pointer) + if count == data.count { + return count + } + if feof(self.pointer) != 0 { + return count + } + if ferror(self.pointer) != 0 { + throw FileError.error(errno) + } + throw FileError.error(0) + } + + public func write(_ data: [UInt8]) throws { + if data.count <= 0 { + return + } + try data.withUnsafeBufferPointer { + if fwrite($0.baseAddress, 1, data.count, self.pointer) != data.count { + throw FileError.error(errno) + } + } + } + + public static func currentWorkingDirectory() throws -> String { + guard let path = getcwd(nil, 0) else { + throw FileError.error(errno) + } + return String(cString: path) + } + } + + public static var pathSeparator = "/" + + public func openNewForWriting() throws -> File { + return try openFileForMode(self, "wb") + } + + public func openForReading() throws -> File { + return try openFileForMode(self, "rb") + } + + public func openForWritingAndReading() throws -> File { + return try openFileForMode(self, "r+b") + } + + public func openFileForMode(_ path: String, _ mode: String) throws -> File { + guard let file = path.withCString({ pathPointer in mode.withCString({ fopen(pathPointer, $0) }) }) else { + throw FileError.error(errno) + } + return File(file) + } + + public func exists() throws -> Bool { + return try self.withStat { + if $0 != nil { + return true + } + return false + } + } + + public func directory() throws -> Bool { + return try self.withStat { + if let stat = $0 { + return stat.st_mode & S_IFMT == S_IFDIR + } + return false + } + } + + public func files() throws -> [String] { + guard let dir = self.withCString({ opendir($0) }) else { + throw FileError.error(errno) + } + defer { closedir(dir) } + var results = [String]() + while let ent = readdir(dir) { + var name = ent.pointee.d_name + let fileName = withUnsafePointer(to: &name) { (ptr) -> String? in + #if os(Linux) + return String(validatingUTF8: ptr.withMemoryRebound(to: CChar.self, capacity: Int(ent.pointee.d_reclen), { (ptrc) -> [CChar] in + return [CChar](UnsafeBufferPointer(start: ptrc, count: 256)) + })) + #else + var buffer = ptr.withMemoryRebound(to: CChar.self, capacity: Int(ent.pointee.d_reclen), { (ptrc) -> [CChar] in + return [CChar](UnsafeBufferPointer(start: ptrc, count: Int(ent.pointee.d_namlen))) + }) + buffer.append(0) + return String(validatingUTF8: buffer) + #endif + } + if let fileName = fileName { + results.append(fileName) + } + } + return results + } + + private func withStat(_ closure: ((stat?) throws -> T)) throws -> T { + return try self.withCString({ + var statBuffer = stat() + if stat($0, &statBuffer) == 0 { + return try closure(statBuffer) + } + if errno == ENOENT { + return try closure(nil) + } + throw FileError.error(errno) + }) + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/String+Misc.swift b/TestTools/StreamChatTestMockServer/Swifter/String+Misc.swift new file mode 100644 index 00000000000..592683c786f --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/String+Misc.swift @@ -0,0 +1,34 @@ +// +// String+Misc.swift +// Swifter +// +// Copyright (c) 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +extension String { + + public func unquote() -> String { + var scalars = self.unicodeScalars + if scalars.first == "\"" && scalars.last == "\"" && scalars.count >= 2 { + scalars.removeFirst() + scalars.removeLast() + return String(scalars) + } + return self + } +} + +extension UnicodeScalar { + + public func asWhitespace() -> UInt8? { + if self.value >= 9 && self.value <= 13 { + return UInt8(self.value) + } + if self.value == 32 { + return UInt8(self.value) + } + return nil + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/String+SHA1.swift b/TestTools/StreamChatTestMockServer/Swifter/String+SHA1.swift new file mode 100644 index 00000000000..b7400bb2f86 --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/String+SHA1.swift @@ -0,0 +1,137 @@ +// +// String+SHA1.swift +// Swifter +// +// Copyright 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +// swiftlint:disable identifier_name function_body_length +public struct SHA1 { + + public static func hash(_ input: [UInt8]) -> [UInt8] { + + // Alghorithm from: https://en.wikipedia.org/wiki/SHA-1 + + var message = input + + var h0 = UInt32(littleEndian: 0x67452301) + var h1 = UInt32(littleEndian: 0xEFCDAB89) + var h2 = UInt32(littleEndian: 0x98BADCFE) + var h3 = UInt32(littleEndian: 0x10325476) + var h4 = UInt32(littleEndian: 0xC3D2E1F0) + + // ml = message length in bits (always a multiple of the number of bits in a character). + + let ml = UInt64(message.count * 8) + + // append the bit '1' to the message e.g. by adding 0x80 if message length is a multiple of 8 bits. + + message.append(0x80) + + // append 0 ≤ k < 512 bits '0', such that the resulting message length in bits is congruent to −64 ≡ 448 (mod 512) + + let padBytesCount = ( message.count + 8 ) % 64 + + message.append(contentsOf: [UInt8](repeating: 0, count: 64 - padBytesCount)) + + // append ml, in a 64-bit big-endian integer. Thus, the total length is a multiple of 512 bits. + + var mlBigEndian = ml.bigEndian + withUnsafePointer(to: &mlBigEndian) { + message.append(contentsOf: Array(UnsafeBufferPointer(start: UnsafePointer(OpaquePointer($0)), count: 8))) + } + + // Process the message in successive 512-bit chunks ( 64 bytes chunks ): + + for chunkStart in 0..(OpaquePointer($0.baseAddress! + (index*4))).pointee}) + words.append(value.bigEndian) + } + + // Extend the sixteen 32-bit words into eighty 32-bit words: + + for index in 16...79 { + let value: UInt32 = ((words[index-3]) ^ (words[index-8]) ^ (words[index-14]) ^ (words[index-16])) + words.append(rotateLeft(value, 1)) + } + + // Initialize hash value for this chunk: + + var a = h0 + var b = h1 + var c = h2 + var d = h3 + var e = h4 + + for i in 0..<80 { + var f = UInt32(0) + var k = UInt32(0) + switch i { + case 0...19: + f = (b & c) | ((~b) & d) + k = 0x5A827999 + case 20...39: + f = b ^ c ^ d + k = 0x6ED9EBA1 + case 40...59: + f = (b & c) | (b & d) | (c & d) + k = 0x8F1BBCDC + case 60...79: + f = b ^ c ^ d + k = 0xCA62C1D6 + default: break + } + let temp = (rotateLeft(a, 5) &+ f &+ e &+ k &+ words[i]) & 0xFFFFFFFF + e = d + d = c + c = rotateLeft(b, 30) + b = a + a = temp + } + + // Add this chunk's hash to result so far: + + h0 = ( h0 &+ a ) & 0xFFFFFFFF + h1 = ( h1 &+ b ) & 0xFFFFFFFF + h2 = ( h2 &+ c ) & 0xFFFFFFFF + h3 = ( h3 &+ d ) & 0xFFFFFFFF + h4 = ( h4 &+ e ) & 0xFFFFFFFF + } + + // Produce the final hash value (big-endian) as a 160 bit number: + + var digest = [UInt8]() + + [h0, h1, h2, h3, h4].forEach { value in + var bigEndianVersion = value.bigEndian + withUnsafePointer(to: &bigEndianVersion) { + digest.append(contentsOf: Array(UnsafeBufferPointer(start: UnsafePointer(OpaquePointer($0)), count: 4))) + } + } + + return digest + } + + private static func rotateLeft(_ v: UInt32, _ n: UInt32) -> UInt32 { + return ((v << n) & 0xFFFFFFFF) | (v >> (32 - n)) + } +} + +extension String { + + public func sha1() -> [UInt8] { + return SHA1.hash([UInt8](self.utf8)) + } + + public func sha1() -> String { + return self.sha1().reduce("") { $0 + String(format: "%02x", $1) } + } +} diff --git a/TestTools/StreamChatTestMockServer/Swifter/WebSockets.swift b/TestTools/StreamChatTestMockServer/Swifter/WebSockets.swift new file mode 100644 index 00000000000..177813adee1 --- /dev/null +++ b/TestTools/StreamChatTestMockServer/Swifter/WebSockets.swift @@ -0,0 +1,295 @@ +// +// HttpHandlers+WebSockets.swift +// Swifter +// +// Copyright © 2014-2016 Damian Kołakowski. All rights reserved. +// + +import Foundation + +@available(*, deprecated, message: "Use websocket(text:binary:pong:connected:disconnected:) instead.") +public func websocket(_ text: @escaping (WebSocketSession, String) -> Void, + _ binary: @escaping (WebSocketSession, [UInt8]) -> Void, + _ pong: @escaping (WebSocketSession, [UInt8]) -> Void) -> ((HttpRequest) -> HttpResponse) { + return websocket(text: text, binary: binary, pong: pong) +} + +// swiftlint:disable function_body_length +public func websocket( + text: ((WebSocketSession, String) -> Void)? = nil, + binary: ((WebSocketSession, [UInt8]) -> Void)? = nil, + pong: ((WebSocketSession, [UInt8]) -> Void)? = nil, + connected: ((WebSocketSession) -> Void)? = nil, + disconnected: ((WebSocketSession) -> Void)? = nil) -> ((HttpRequest) -> HttpResponse) { + return { request in + guard request.hasTokenForHeader("upgrade", token: "websocket") else { + return .badRequest(.text("Invalid value of 'Upgrade' header: \(request.headers["upgrade"] ?? "unknown")")) + } + guard request.hasTokenForHeader("connection", token: "upgrade") else { + return .badRequest(.text("Invalid value of 'Connection' header: \(request.headers["connection"] ?? "unknown")")) + } + guard let secWebSocketKey = request.headers["sec-websocket-key"] else { + return .badRequest(.text("Invalid value of 'Sec-Websocket-Key' header: \(request.headers["sec-websocket-key"] ?? "unknown")")) + } + let protocolSessionClosure: ((Socket) -> Void) = { socket in + let session = WebSocketSession(socket) + var fragmentedOpCode = WebSocketSession.OpCode.close + var payload = [UInt8]() // Used for fragmented frames. + + func handleTextPayload(_ frame: WebSocketSession.Frame) throws { + if let handleText = text { + if frame.fin { + if payload.count > 0 { + throw WebSocketSession.WsError.protocolError("Continuing fragmented frame cannot have an operation code.") + } + var textFramePayload = frame.payload.map { Int8(bitPattern: $0) } + textFramePayload.append(0) + if let text = String(validatingUTF8: textFramePayload) { + handleText(session, text) + } else { + throw WebSocketSession.WsError.invalidUTF8("") + } + } else { + payload.append(contentsOf: frame.payload) + fragmentedOpCode = .text + } + } + } + + func handleBinaryPayload(_ frame: WebSocketSession.Frame) throws { + if let handleBinary = binary { + if frame.fin { + if payload.count > 0 { + throw WebSocketSession.WsError.protocolError("Continuing fragmented frame cannot have an operation code.") + } + handleBinary(session, frame.payload) + } else { + payload.append(contentsOf: frame.payload) + fragmentedOpCode = .binary + } + } + } + + func handleOperationCode(_ frame: WebSocketSession.Frame) throws { + switch frame.opcode { + case .continue: + // There is no message to continue, failed immediatelly. + if fragmentedOpCode == .close { + socket.close() + } + frame.opcode = fragmentedOpCode + if frame.fin { + payload.append(contentsOf: frame.payload) + frame.payload = payload + // Clean the buffer. + payload = [] + // Reset the OpCode. + fragmentedOpCode = WebSocketSession.OpCode.close + } + try handleOperationCode(frame) + case .text: + try handleTextPayload(frame) + case .binary: + try handleBinaryPayload(frame) + case .close: + throw WebSocketSession.Control.close + case .ping: + if frame.payload.count > 125 { + throw WebSocketSession.WsError.protocolError("Payload gretter than 125 octets.") + } else { + session.writeFrame(ArraySlice(frame.payload), .pong) + } + case .pong: + if let handlePong = pong { + handlePong(session, frame.payload) + } + } + } + + func read() throws { + while true { + let frame = try session.readFrame() + try handleOperationCode(frame) + } + } + + connected?(session) + + do { + try read() + } catch let error { + switch error { + case WebSocketSession.Control.close: + // Normal close + break + case WebSocketSession.WsError.unknownOpCode: + print("Unknown Op Code: \(error)") + case WebSocketSession.WsError.unMaskedFrame: + print("Unmasked frame: \(error)") + case WebSocketSession.WsError.invalidUTF8: + print("Invalid UTF8 character: \(error)") + case WebSocketSession.WsError.protocolError: + print("Protocol error: \(error)") + default: + print("Unkown error \(error)") + } + // If an error occurs, send the close handshake. + session.writeCloseFrame() + } + + disconnected?(session) + } + let secWebSocketAccept = String.toBase64((secWebSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").sha1()) + let headers = ["Upgrade": "WebSocket", "Connection": "Upgrade", "Sec-WebSocket-Accept": secWebSocketAccept] + return HttpResponse.switchProtocols(headers, protocolSessionClosure) + } +} + +public class WebSocketSession: Hashable, Equatable { + + public enum WsError: Error { case unknownOpCode(String), unMaskedFrame(String), protocolError(String), invalidUTF8(String) } + public enum OpCode: UInt8 { case `continue` = 0x00, close = 0x08, ping = 0x09, pong = 0x0A, text = 0x01, binary = 0x02 } + public enum Control: Error { case close } + + public class Frame { + public var opcode = OpCode.close + public var fin = false + public var rsv1: UInt8 = 0 + public var rsv2: UInt8 = 0 + public var rsv3: UInt8 = 0 + public var payload = [UInt8]() + } + + public let socket: Socket + + public init(_ socket: Socket) { + self.socket = socket + } + + deinit { + writeCloseFrame() + socket.close() + } + + public func writeText(_ text: String) { + self.writeFrame(ArraySlice(text.utf8), OpCode.text) + } + + public func writeBinary(_ binary: [UInt8]) { + self.writeBinary(ArraySlice(binary)) + } + + public func writeBinary(_ binary: ArraySlice) { + self.writeFrame(binary, OpCode.binary) + } + + public func writeFrame(_ data: ArraySlice, _ op: OpCode, _ fin: Bool = true) { + let finAndOpCode = UInt8(fin ? 0x80 : 0x00) | op.rawValue + let maskAndLngth = encodeLengthAndMaskFlag(UInt64(data.count), false) + do { + try self.socket.writeUInt8([finAndOpCode]) + try self.socket.writeUInt8(maskAndLngth) + try self.socket.writeUInt8(data) + } catch { + print(error) + } + } + + public func writeCloseFrame() { + writeFrame(ArraySlice("".utf8), .close) + } + + private func encodeLengthAndMaskFlag(_ len: UInt64, _ masked: Bool) -> [UInt8] { + let encodedLngth = UInt8(masked ? 0x80 : 0x00) + var encodedBytes = [UInt8]() + switch len { + case 0...125: + encodedBytes.append(encodedLngth | UInt8(len)) + case 126...UInt64(UINT16_MAX): + encodedBytes.append(encodedLngth | 0x7E) + encodedBytes.append(UInt8(len >> 8 & 0xFF)) + encodedBytes.append(UInt8(len >> 0 & 0xFF)) + default: + encodedBytes.append(encodedLngth | 0x7F) + encodedBytes.append(UInt8(len >> 56 & 0xFF)) + encodedBytes.append(UInt8(len >> 48 & 0xFF)) + encodedBytes.append(UInt8(len >> 40 & 0xFF)) + encodedBytes.append(UInt8(len >> 32 & 0xFF)) + encodedBytes.append(UInt8(len >> 24 & 0xFF)) + encodedBytes.append(UInt8(len >> 16 & 0xFF)) + encodedBytes.append(UInt8(len >> 08 & 0xFF)) + encodedBytes.append(UInt8(len >> 00 & 0xFF)) + } + return encodedBytes + } + + // swiftlint:disable function_body_length + public func readFrame() throws -> Frame { + let frm = Frame() + let fst = try socket.read() + frm.fin = fst & 0x80 != 0 + frm.rsv1 = fst & 0x40 + frm.rsv2 = fst & 0x20 + frm.rsv3 = fst & 0x10 + guard frm.rsv1 == 0 && frm.rsv2 == 0 && frm.rsv3 == 0 + else { + throw WsError.protocolError("Reserved frame bit has not been negociated.") + } + let opc = fst & 0x0F + guard let opcode = OpCode(rawValue: opc) else { + // "If an unknown opcode is received, the receiving endpoint MUST _Fail the WebSocket Connection_." + // http://tools.ietf.org/html/rfc6455#section-5.2 ( Page 29 ) + throw WsError.unknownOpCode("\(opc)") + } + if frm.fin == false { + switch opcode { + case .ping, .pong, .close: + // Control frames must not be fragmented + // https://tools.ietf.org/html/rfc6455#section-5.5 ( Page 35 ) + throw WsError.protocolError("Control frames must not be fragmented.") + default: + break + } + } + frm.opcode = opcode + let sec = try socket.read() + let msk = sec & 0x80 != 0 + guard msk else { + // "...a client MUST mask all frames that it sends to the server." + // http://tools.ietf.org/html/rfc6455#section-5.1 + throw WsError.unMaskedFrame("A client must mask all frames that it sends to the server.") + } + var len = UInt64(sec & 0x7F) + if len == 0x7E { + let b0 = UInt64(try socket.read()) << 8 + let b1 = UInt64(try socket.read()) + len = UInt64(littleEndian: b0 | b1) + } else if len == 0x7F { + let b0 = UInt64(try socket.read()) << 54 + let b1 = UInt64(try socket.read()) << 48 + let b2 = UInt64(try socket.read()) << 40 + let b3 = UInt64(try socket.read()) << 32 + let b4 = UInt64(try socket.read()) << 24 + let b5 = UInt64(try socket.read()) << 16 + let b6 = UInt64(try socket.read()) << 8 + let b7 = UInt64(try socket.read()) + len = UInt64(littleEndian: b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7) + } + + let mask = [try socket.read(), try socket.read(), try socket.read(), try socket.read()] + // Read payload all at once, then apply mask (calling `socket.read` byte-by-byte is super slow). + frm.payload = try socket.read(length: Int(len)) + for index in 0.. Bool { + return webSocketSession1.socket == webSocketSession2.socket +} diff --git a/TestTools/StreamChatTestMockServer/Utilities/TestData.swift b/TestTools/StreamChatTestMockServer/Utilities/TestData.swift index 3f13dd42109..23a6d87664e 100644 --- a/TestTools/StreamChatTestMockServer/Utilities/TestData.swift +++ b/TestTools/StreamChatTestMockServer/Utilities/TestData.swift @@ -3,7 +3,6 @@ // @testable import StreamChat -import Swifter import XCTest public enum TestData { diff --git a/TestTools/StreamChatTestTools/Assertions/AssertAsync.swift b/TestTools/StreamChatTestTools/Assertions/AssertAsync.swift new file mode 100644 index 00000000000..62dedf40055 --- /dev/null +++ b/TestTools/StreamChatTestTools/Assertions/AssertAsync.swift @@ -0,0 +1,566 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import XCTest + +// MARK: - Assertions + +/// Internal representation for assertions. Not meant to be created and used directly. If you wan't to add an assertion, +/// add a new static func to `extension Assert { }` and create the `Assertion` object inside. +public struct Assertion { + public enum State { + case active, idle + } + + public let body: (_ elapsedTime: TimeInterval) -> State + + /// Evaluates the assertion. + public func evaluate(elapsedTime: TimeInterval) -> State { + body(elapsedTime) + } +} + +// Syntax sugar to make assertion code more readable: +// `Assert.willBeTrue(expression)` vs `Assertion.willBeTrue(true)` +public typealias Assert = Assertion + +public extension Assert { + /// Periodically checks for the equality of the provided expressions. Fails if the expression results are not + /// equal within the `timeout` period. + /// + /// - Parameters: + /// - expression1: The first expression to evaluate. + /// - expression2: The first expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ Both expressions are evaluated repeatedly during the function execution. The expressions should not have + /// any side effects which can affect their results. + static func willBeEqual( + _ expression1: @autoclosure @escaping () -> T, + _ expression2: @autoclosure @escaping () -> T, + timeout: TimeInterval = defaultTimeout, + message: @autoclosure @escaping () -> String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) -> Assertion { + // We can't use this as the default parameter because of the string interpolation. + var defaultMessage: String { + "Found difference for \n" + diff(expression1(), expression2()).joined(separator: ", ") + } + + return willBeTrue( + expression1() == expression2(), + timeout: timeout, + message: message() ?? defaultMessage, + file: file, + line: line + ) + } + + /// Periodically checks if the expression evaluates to `nil`. Fails if the expression result is not `nil` within + /// the `timeout` period. + /// + /// - Parameters: + /// - expression: The expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ The expression is evaluated repeatedly during the function execution. It should not have + /// any side effects which can affect its result. + static func willBeNil( + _ expression1: @autoclosure @escaping () -> T?, + timeout: TimeInterval = defaultTimeout, + message: @autoclosure () -> String = "Failed to become `nil`", + file: StaticString = #filePath, + line: UInt = #line + ) -> Assertion { + willBeTrue( + expression1() == nil, + timeout: timeout, + message: "Failed to become `nil`", + file: file, + line: line + ) + } + + /// Periodically checks if the expression does not evaluate to `nil`. Fails if the expression result is `nil` within + /// the `timeout` period. + /// + /// - Parameters: + /// - expression: The expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ The expression is evaluated repeatedly during the function execution. It should not have + /// any side effects which can affect its result. + static func willNotBeNil( + _ expression1: @autoclosure @escaping () -> T?, + timeout: TimeInterval = defaultTimeout, + message: @autoclosure () -> String = "Failed to not be `nil`", + file: StaticString = #filePath, + line: UInt = #line + ) -> Assertion { + willBeTrue( + expression1() != nil, + timeout: timeout, + message: "Failed to not be `nil`", + file: file, + line: line + ) + } + + /// Periodically checks if the expression evaluates to `TRUE`. Fails if the expression result is not `TRUE` within + /// the `timeout` period. + /// + /// - Parameters: + /// - expression: The expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ The expression is evaluated repeatedly during the function execution. It should not have + /// any side effects which can affect its result. + static func willBeTrue( + _ expression: @autoclosure @escaping () -> Bool, + timeout: TimeInterval = defaultTimeout, + message: @autoclosure @escaping () -> String = "Failed to become `TRUE`", + file: StaticString = #filePath, + line: UInt = #line + ) -> Assertion { + Assertion { elapsedTime in + if elapsedTime < timeout { + if expression() { + // Success + return .idle + } + } else { + // Timeout + XCTFail(message(), file: file, line: line) + return .idle + } + + return .active + } + } + + /// Periodically checks if the expression evaluates to `FALSE`. Fails if the expression result is not `FALSE` within + /// the `timeout` period. + /// + /// - Parameters: + /// - expression: The expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ The expression is evaluated repeatedly during the function execution. It should not have + /// any side effects which can affect its result. + static func willBeFalse( + _ expression: @autoclosure @escaping () -> Bool, + timeout: TimeInterval = defaultTimeout, + message: @autoclosure @escaping () -> String = "Failed to become `TRUE`", + file: StaticString = #filePath, + line: UInt = #line + ) -> Assertion { + willBeTrue( + !expression(), + timeout: timeout, + message: "Failed to become `FALSE`", + file: file, + line: line + ) + } + + /// Periodically checks that the expression evaluates stays `FALSE` for the whole `timeout` period.. Fails if the expression + /// becommes `TRUE` before the end of the `timeout` period. + /// + /// - Parameters: + /// - expression: The expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ The expression is evaluated repeatedly during the function execution. It should not have + /// any side effects which can affect its result. + static func staysFalse( + _ expression: @autoclosure @escaping () -> Bool, + timeout: TimeInterval = defaultTimeoutForInversedExpecations, + message: @autoclosure @escaping () -> String = "Failed to stay `FALSE`", + file: StaticString = #filePath, + line: UInt = #line + ) -> Assertion { + staysTrue(!expression(), timeout: timeout, message: message(), file: file, line: line) + } + + /// Periodically checks that the expression evaluates stays `TRUE` for the whole `timeout` period.. Fails if the expression + /// becommes `FALSE` before the end of the `timeout` period. + /// + /// - Parameters: + /// - expression: The expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ The expression is evaluated repeatedly during the function execution. It should not have + /// any side effects which can affect its result. + static func staysTrue( + _ expression: @autoclosure @escaping () -> Bool, + timeout: TimeInterval = defaultTimeoutForInversedExpecations, + message: @autoclosure @escaping () -> String = "Failed to stay `TRUE`", + file: StaticString = #filePath, + line: UInt = #line + ) -> Assertion { + Assertion { elapsedTime in + if elapsedTime >= timeout { + // Success + return .idle + + } else if expression() == false { + // Failure + XCTFail(message(), file: file, line: line) + return .idle + } + + return .active + } + } + + /// Blocks the current test execution and periodically checks for the equality of the provided expressions for + /// the whole `timeout` period. Fails if the expression results are not equal before the end of the `timeout` period. + /// + /// - Parameters: + /// - expression1: The first expression to evaluate. + /// - expression2: The first expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ Both expressions are evaluated repeatedly during the function execution. The expressions should not have + /// any side effects which can affect their results. + static func staysEqual( + _ expression1: @autoclosure @escaping () -> T, + _ expression2: @autoclosure @escaping () -> T, + timeout: TimeInterval = defaultTimeoutForInversedExpecations, + message: @autoclosure @escaping () -> String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) -> Assertion { + // We can't use this as the default parameter because of the string interpolation. + var defaultMessage: String { + "\"\(String(describing: expression1()))\" failed to stay equal to \"\(String(describing: expression2()))\"" + } + + return staysTrue( + expression1() == expression2(), + timeout: timeout, + message: message() ?? defaultMessage, + file: file, + line: line + ) + } + + /// Blocks the current test execution and asynchronously checks if the provided object can be released from the memobry + /// by assigning it to `nil`. + /// + /// - Warning: ⚠️ The object is destroyed during the proccess and the provided inout variable is set to `nil`, so you + /// can't use it after this assertions has finished. + /// + /// - Parameters: + /// - object: The object to check for retain cycles. + /// - timeout: The maximum time the function waits for the object to be released. + /// - message: The message to print when the assertion fails. + static func canBeReleased( + _ object: inout T!, + timeout: TimeInterval = defaultTimeout, + message: @autoclosure @escaping () -> String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) -> Assertion { + weak var weakObject: T? = object + object = nil + + return willBeNil(weakObject, message: "Failed to be released from the memory.", file: file, line: line) + } +} + +// MARK: - Async runner + +/// Allows creating and evaluating asynchronous assertions with synchronous-like syntax. When called, stops the test +/// execution and waits for the assertions to be fulfilled. +/// +/// There are two ways of using `AssertAsync`: +/// - For single assertions, you can use it directly using the convenience static function helpers: +/// ``` +/// func test() { +/// // ... +/// AssertAsync.willBeNil(expression) +/// } +/// ``` +/// +/// - If you have multiple assertions you want to evaluate at the same time, you can use the following syntax: +/// ``` +/// func test() { +/// // ... +/// AssertAsync { +/// Assert.willBeNil(expression1) +/// Assert.staysFalse(expression2) +/// } +/// } +/// ``` +public struct AssertAsync { + // This shouldn't be needed and it's just a workaround for https://bugs.swift.org/browse/SR-11628. Remove when possible. + @discardableResult + public init(@AssertionBuilder singleBuilder: () -> Assertion) { + self.init(builder: { + let built = singleBuilder() + return [built] + }) + } + + @discardableResult + public init(@AssertionBuilder builder: () -> [Assertion]) { + var assertions = builder() + let startTimestamp = Date().timeIntervalSince1970 + + while assertions.isEmpty == false { + let elapsedTime = Date().timeIntervalSince1970 - startTimestamp + // Evaluate and remove idle assertions + assertions = assertions.filter { $0.evaluate(elapsedTime: elapsedTime) == .active } + _ = XCTWaiter.wait(for: [.init()], timeout: evaluationPeriod) + } + } +} + +@resultBuilder +public enum AssertionBuilder { + public static func buildBlock(_ assertion: Assertion) -> Assertion { + assertion + } + + public static func buildBlock(_ assertions: Assertion...) -> [Assertion] { + assertions + } +} + +public extension AssertAsync { + /// Periodically checks if the expression evaluates to `TRUE`. Fails if the expression result is not `TRUE` within + /// the `timeout` period. + /// + /// - Parameters: + /// - expression: The expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ Both expressions are evaluated repeatedly during the function execution. The expressions should not have + /// any side effects which can affect their results. + static func willBeTrue( + _ expression: @autoclosure () -> Bool?, + timeout: TimeInterval = defaultTimeout, + message: @autoclosure () -> String = "Failed to become `TRUE`", + file: StaticString = #filePath, + line: UInt = #line + ) { + _ = withoutActuallyEscaping(expression) { expression in + withoutActuallyEscaping(message) { message in + + AssertAsync { + Assert.willBeEqual( + expression(), + true, + timeout: timeout, + message: message(), + file: file, + line: line + ) + } + } + } + } + + /// Periodically checks if the expression evaluates to `FALSE`. Fails if the expression result is not `FALSE` within + /// the `timeout` period. + /// + /// - Parameters: + /// - expression: The expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ Both expressions are evaluated repeatedly during the function execution. The expressions should not have + /// any side effects which can affect their results. + static func willBeFalse( + _ expression: @autoclosure () -> Bool?, + timeout: TimeInterval = defaultTimeout, + message: @autoclosure () -> String = "Failed to become `FALSE`", + file: StaticString = #filePath, + line: UInt = #line + ) { + _ = withoutActuallyEscaping(expression) { expression in + withoutActuallyEscaping(message) { message in + + AssertAsync { + Assert.willBeEqual( + expression(), + false, + timeout: timeout, + message: message(), + file: file, + line: line + ) + } + } + } + } + + /// Blocks the current test execution and periodically checks for the equality of the provided expressions. Fails if + /// the expression results are not equal within the `timeout` period. + /// + /// - Parameters: + /// - expression1: The first expression to evaluate. + /// - expression2: The first expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ Both expressions are evaluated repeatedly during the function execution. The expressions should not have + /// any side effects which can affect their results. + static func willBeEqual( + _ expression1: @autoclosure () -> T, + _ expression2: @autoclosure () -> T, + timeout: TimeInterval = defaultTimeout, + message: @autoclosure () -> String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) { + _ = withoutActuallyEscaping(expression1) { expression1 in + withoutActuallyEscaping(expression2) { expression2 in + withoutActuallyEscaping(message) { message in + + AssertAsync { + Assert.willBeEqual( + expression1(), + expression2(), + timeout: timeout, + message: message(), + file: file, + line: line + ) + } + } + } + } + } + + /// Blocks the current test execution and periodically checks if the expression evaluates to `nil`. Fails if + /// the expression result is not `nil` within the `timeout` period. + /// + /// - Parameters: + /// - expression: The expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ The expression is evaluated repeatedly during the function execution. It should not have + /// any side effects which can affect its result. + static func willBeNil( + _ expression: @autoclosure () -> T?, + timeout: TimeInterval = defaultTimeout, + message: @autoclosure () -> String = "Failed to become `nil`", + file: StaticString = #filePath, + line: UInt = #line + ) { + _ = withoutActuallyEscaping(expression) { expression in + withoutActuallyEscaping(message) { message in + + AssertAsync { + Assert.willBeTrue( + expression() == nil, + timeout: timeout, + message: message(), + file: file, + line: line + ) + } + } + } + } + + /// Blocks the current test execution and periodically checks that the expression evaluates stays `TRUE` for + /// the whole `timeout` period. Fails if the expression becommes `FALSE` before the end of the `timeout` period. + /// + /// - Parameters: + /// - expression: The expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ The expression is evaluated repeatedly during the function execution. It should not have + /// any side effects which can affect its result. + static func staysTrue( + _ expression: @autoclosure () -> Bool, + timeout: TimeInterval = defaultTimeoutForInversedExpecations, + message: @autoclosure () -> String = "Failed to stay `TRUE`", + file: StaticString = #filePath, + line: UInt = #line + ) { + _ = withoutActuallyEscaping(expression) { expression in + withoutActuallyEscaping(message) { message in + AssertAsync { + Assert.staysTrue(expression(), timeout: timeout, message: message(), file: file, line: line) + } + } + } + } + + /// Blocks the current test execution and periodically checks for the equality of the provided expressions for + /// the whole `timeout` period. Fails if the expression results are not equal before the end of the `timeout` period. + /// + /// - Parameters: + /// - expression1: The first expression to evaluate. + /// - expression2: The first expression to evaluate. + /// - timeout: The maximum time the function waits for the expression results to equal. + /// - message: The message to print when the assertion fails. + /// + /// - Warning: ⚠️ Both expressions are evaluated repeatedly during the function execution. The expressions should not have + /// any side effects which can affect their results. + static func staysEqual( + _ expression1: @autoclosure () -> T, + _ expression2: @autoclosure () -> T, + timeout: TimeInterval = defaultTimeoutForInversedExpecations, + message: @autoclosure () -> String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) { + _ = withoutActuallyEscaping(expression1) { expression1 in + withoutActuallyEscaping(expression2) { expression2 in + withoutActuallyEscaping(message) { message in + + AssertAsync { + Assert.staysEqual( + expression1(), + expression2(), + timeout: timeout, + message: message(), + file: file, + line: line + ) + } + } + } + } + } + + /// Blocks the current test execution and asynchronously checks if the provided object can be released from the memobry + /// by assigning it to `nil`. + /// + /// - Warning: ⚠️ The object is destroyed during the proccess and the provided inout variable is set to `nil`, so you + /// can't use it after this assertions has finished. + /// + /// - Parameters: + /// - object: The object to check for retain cycles. + /// - timeout: The maximum time the function waits for the object to be released. + /// - message: The message to print when the assertion fails. + static func canBeReleased( + _ object: inout T!, + timeout: TimeInterval = defaultTimeout, + message: @autoclosure @escaping () -> String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) { + AssertAsync { + Assert.canBeReleased(&object, timeout: timeout, message: message(), file: file, line: line) + } + } +} diff --git a/TestTools/StreamChatTestTools/Assertions/AssertDate.swift b/TestTools/StreamChatTestTools/Assertions/AssertDate.swift new file mode 100644 index 00000000000..2b7fb23e96e --- /dev/null +++ b/TestTools/StreamChatTestTools/Assertions/AssertDate.swift @@ -0,0 +1,28 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import XCTest + +public func XCTAssertNearlySameDate(_ lhs: Date, _ rhs: Date, file: StaticString = #filePath, line: UInt = #line) { + XCTAssertTrue(lhs.isNearlySameDate(as: rhs)) +} + +public func XCTAssertNearlySameDate(_ lhs: Date?, _ rhs: Date?, file: StaticString = #filePath, line: UInt = #line) { + if lhs == nil && rhs == nil { + XCTAssertTrue(true, file: file, line: line) + } + + guard let lhs = lhs, let rhs = rhs else { + XCTAssertEqual(lhs, rhs, file: file, line: line) + return + } + + XCTAssertNearlySameDate(lhs, rhs, file: file, line: line) +} + +extension Date { + public func isNearlySameDate(as otherDate: Date) -> Bool { + (self - 0.01)...(self + 0.01) ~= otherDate + } +} diff --git a/TestTools/StreamChatTestTools/Assertions/AssertJSONEqual.swift b/TestTools/StreamChatTestTools/Assertions/AssertJSONEqual.swift new file mode 100644 index 00000000000..75e834aaa01 --- /dev/null +++ b/TestTools/StreamChatTestTools/Assertions/AssertJSONEqual.swift @@ -0,0 +1,173 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import Foundation +import XCTest + +/// Helper function for creating NSError. +public func error(domain: String, code: Int = -1, message: @autoclosure () -> String) -> NSError { + NSError(domain: domain, code: code, userInfo: ["message:": message()]) +} + +/// Compares the given 2 JSON Serializations are equal, by creating JSON objects from Data and comparing dictionaries. +/// Recursively calls itself for nested dictionaries. +/// - Parameters: +/// - expression1: JSON object 1, as Data. From string, you can do `Data(jsonString.utf8)` +/// - expression2: JSON object 2. +public func CompareJSONEqual( + _ expression1: @autoclosure () throws -> Data, + _ expression2: @autoclosure () throws -> [String: Any] +) throws { + guard var json = try JSONSerialization.jsonObject(with: expression1()) as? [String: Any] else { + throw error(domain: "CompareJSONEqual", message: "The first expression is not a valid json object!") + } + + preprocessBoolValues(&json) + + try CompareJSONEqual(json, try expression2()) +} + +/// Asserts the given 2 JSON Serializations are equal, by creating JSON objects from Data and comparing dictionaries. +/// Recursively calls itself for nested dictionaries. +/// - Parameters: +/// - expression1: JSON object 1, as Data. From string, you can do `Data(jsonString.utf8)` +/// - expression2: JSON object 2. +/// - file: file the assert is being made +/// - line: line the assert is being made. +public func AssertJSONEqual( + _ expression1: @autoclosure () throws -> Data, + _ expression2: @autoclosure () throws -> [String: Any], + file: StaticString = #filePath, + line: UInt = #line +) { + do { + try CompareJSONEqual(expression1(), expression2()) + } catch { + XCTFail("Error: \(error)", file: file, line: line) + } +} + +/// Compares the given 2 JSON Serializations are equal, by creating JSON objects from Data and comparing dictionaries. +/// Recursively calls itself for nested dictionaries. +/// - Parameters: +/// - expression1: JSON object 1, as Data. From string, you can do `Data(jsonString.utf8)` +/// - expression2: JSON object 2, as Data. +public func CompareJSONEqual( + _ expression1: @autoclosure () throws -> Data, + _ expression2: @autoclosure () throws -> Data +) throws { + guard let json1 = try JSONSerialization.jsonObject(with: expression1()) as? [String: Any] else { + throw error(domain: "CompareJSONEqual", message: "First expression is not a valid json object!") + } + guard let json2 = try JSONSerialization.jsonObject(with: expression2()) as? [String: Any] else { + throw error(domain: "CompareJSONEqual", message: "Second expression is not a valid json object!") + } + + try CompareJSONEqual(json1, json2) +} + +/// Asserts the given 2 JSON Serializations are equal, by creating JSON objects from Data and comparing dictionaries. +/// Recursively calls itself for nested dictionaries. +/// - Parameters: +/// - expression1: JSON object 1, as Data. From string, you can do `Data(jsonString.utf8)` +/// - expression2: JSON object 2, as Data. +/// - file: file the assert is being made +/// - line: line the assert is being made. +public func AssertJSONEqual( + _ expression1: @autoclosure () throws -> Data, + _ expression2: @autoclosure () throws -> Data, + file: StaticString = #filePath, + line: UInt = #line +) { + do { + try CompareJSONEqual(expression1(), expression2()) + } catch { + XCTFail("Error: \(error)", file: file, line: line) + } +} + +/// Compares the given 2 JSON Serializations are equal, by creating JSON objects from Data and comparing dictionaries. +/// Recursively calls itself for nested dictionaries. +/// - Parameters: +/// - expression1: JSON object 1 +/// - expression2: JSON object 2 +public func CompareJSONEqual( + _ expression1: @autoclosure () throws -> [String: Any], + _ expression2: @autoclosure () throws -> [String: Any] +) throws { + do { + let json1 = try expression1() + let json2 = try expression2() + + guard json1.keys == json2.keys else { + throw error( + domain: "CompareJSONEqual", + message: "JSON keys do not match. Expression 1 keys: \(json1.keys), Expression 2 keys: \(json2.keys)" + ) + } + + try json1.forEach { key, value in + guard let value2 = json2[key] else { + throw error(domain: "CompareJSONEqual", message: "Expression 2 does not have value for \(key)") + } + if let nestedDict1 = value as? [String: Any] { + if let nestedDict2 = value2 as? [String: Any] { + try CompareJSONEqual( + JSONSerialization.data(withJSONObject: nestedDict1), + JSONSerialization.data(withJSONObject: nestedDict2) + ) + } else { + throw error( + domain: "CompareJSONEqual", + message: "Nested values for key \(key) do not match. " + + "Expression 1 value: \(value), Expression 2 value: \(value2)" + ) + } + } else if String(describing: value) != String(describing: value2) { + // If you get a failure here because your values are arrays, you should + // change how you encode the [String: Any] dictionary. Please see + // `test_channelEditDetailPayload_encodedCorrectly` for more info + throw error( + domain: "CompareJSONEqual", + message: "Values for key \(key) do not match. " + + "Expression 1 value: \(value), Expression 2 value: \(value2)" + ) + } + } + } catch { + throw error + } +} + +/// Asserts the given 2 JSON Serializations are equal, by creating JSON objects from Data and comparing dictionaries. +/// Recursively calls itself for nested dictionaries. +/// - Parameters: +/// - expression1: JSON object 1 +/// - expression2: JSON object 2 +/// - file: file the assert is being made +/// - line: line the assert is being made. +public func AssertJSONEqual( + _ expression1: @autoclosure () throws -> [String: Any], + _ expression2: @autoclosure () throws -> [String: Any], + file: StaticString = #filePath, + line: UInt = #line +) { + do { + try CompareJSONEqual(expression1(), expression2()) + } catch { + XCTFail("Error: \(error)", file: file, line: line) + } +} + +/// A helper function that converts Bool values to their string representations "true"/"false". Needed to unify the way +/// JSON is represented in Objective-C and Swift. Objective-C represents true as `1` while Swift doest it like `true`. +private func preprocessBoolValues(_ json: inout [String: Any]) { + var newKeys: [String: Any] = [:] + json.forEach { (key, value) in + if let value = value as? Bool { + newKeys[key] = value ? "true" : "false" + } + } + json.merge(newKeys, uniquingKeysWith: { _, new in new }) +} diff --git a/TestTools/StreamChatTestTools/AssertNetworkRequest.swift b/TestTools/StreamChatTestTools/Assertions/AssertNetworkRequest.swift similarity index 100% rename from TestTools/StreamChatTestTools/AssertNetworkRequest.swift rename to TestTools/StreamChatTestTools/Assertions/AssertNetworkRequest.swift diff --git a/TestTools/StreamChatTestTools/Assertions/AssertResult.swift b/TestTools/StreamChatTestTools/Assertions/AssertResult.swift new file mode 100644 index 00000000000..41ca4a02d8a --- /dev/null +++ b/TestTools/StreamChatTestTools/Assertions/AssertResult.swift @@ -0,0 +1,62 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import XCTest + +/// Asserts the provided `Result` object is `.success(referenceValue)`. +/// +/// - Parameters: +/// - result: The Result object under tests. +/// - referenceValue: The reference success value. +public func AssertResultSuccess( + _ result: Result, + _ referenceValue: T, + file: StaticString = #filePath, + line: UInt = #line +) { + if case let .success(value) = result { + XCTAssertEqual(value, referenceValue, file: file, line: line) + } else { + XCTFail("Expected .success, got \(result).", file: file, line: line) + } +} + +/// Asserts the provided `Result` object is `.failure(referenceError)`. +/// +/// - Parameters: +/// - result: The Result object under tests. +/// - referenceError: The reference failure value. +/// +/// - Warning: ⚠️ Because the `Error` type is not `Equatable`, it compares the error objects +/// based on their description generated by `String(describing:)`. +public func AssertResultFailure( + _ result: Result, + _ referenceError: Error, + file: StaticString = #filePath, + line: UInt = #line +) { + if case let .failure(error) = result { + XCTAssertEqual(String(describing: error), String(describing: referenceError), file: file, line: line) + } else { + XCTFail("Expected .failure, got \(result).", file: file, line: line) + } +} + +/// Asserts the provided `Result` object is `.failure(referenceError)`. +/// +/// - Parameters: +/// - result: The Result object under tests. +/// - referenceError: The reference failure value. +public func AssertResultFailure( + _ result: Result, + _ referenceError: E, + file: StaticString = #filePath, + line: UInt = #line +) { + if case let .failure(error) = result { + XCTAssertEqual(error, referenceError, file: file, line: line) + } else { + XCTFail("Expected .failure, got \(result).", file: file, line: line) + } +} diff --git a/TestTools/StreamChatTestTools/Assertions/AssertTestQueue.swift b/TestTools/StreamChatTestTools/Assertions/AssertTestQueue.swift new file mode 100644 index 00000000000..5a2f63caa4e --- /dev/null +++ b/TestTools/StreamChatTestTools/Assertions/AssertTestQueue.swift @@ -0,0 +1,32 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import Foundation +import XCTest + +/// Asserts that the queue the function is executed on, is the test queue with the given UUID. +/// +/// - Parameters: +/// - id: The `id` of the expected test queue. +public func AssertTestQueue(withId id: UUID, file: StaticString = #filePath, line: UInt = #line) { + if !DispatchQueue.isTestQueue(withId: id) { + XCTFail("The current queue doesn't match the expected queue.", file: file, line: line) + } +} + +public extension DispatchQueue { + private static let queueIdKey = DispatchSpecificKey() + + /// Creates a new queue which can be later identified by the id. + static func testQueue(withId id: UUID) -> DispatchQueue { + let queue = DispatchQueue(label: "Test queue: <\(id)>") + queue.setSpecific(key: Self.queueIdKey, value: id.uuidString) + return queue + } + + /// Checks if the current queue is the queue with the given id. + static func isTestQueue(withId id: UUID) -> Bool { + DispatchQueue.getSpecific(key: queueIdKey) == id.uuidString + } +} diff --git a/TestTools/StreamChatTestTools/Assertions/UnwrapAsync.swift b/TestTools/StreamChatTestTools/Assertions/UnwrapAsync.swift new file mode 100644 index 00000000000..e35ec9cf531 --- /dev/null +++ b/TestTools/StreamChatTestTools/Assertions/UnwrapAsync.swift @@ -0,0 +1,32 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import XCTest + +/// Allows synchronously waiting for the provided value to become non-nil and returns the unwrap value. +/// +/// Example usage: +/// ``` +/// var someComputedValue: T? { ... } +/// let unwrapped: T = try unwrapAsync(someComputedValue) +/// ``` +/// +/// - Parameters: +/// - timeout: The maximum time this function waits for the value to become non-nil. +/// - valueToBeUnwrapped: The value which is periodically check for becoming non-nil. +/// +/// - Throws: `WaiterError.waitingForResultTimedOut` if `valueToBeUnwrapped` doesn't become non-nil +/// within the `timeout` period. +/// +/// - Returns: The unwrapped value. +public func unwrapAsync( + timeout: TimeInterval = defaultTimeout, + file: StaticString = #filePath, + line: UInt = #line, + _ valueToBeUnwrapped: @autoclosure (() -> T?) +) throws -> T { + var value: T? { valueToBeUnwrapped() } + AssertAsync.willBeTrue(value != nil, message: "Failed to unwrap the value within the specied timeout.", file: file, line: line) + return value! +} diff --git a/TestTools/StreamChatTestTools/Difference/Difference.swift b/TestTools/StreamChatTestTools/Difference/Difference.swift new file mode 100644 index 00000000000..9027b03f3b2 --- /dev/null +++ b/TestTools/StreamChatTestTools/Difference/Difference.swift @@ -0,0 +1,460 @@ +// +// Difference.swift +// Difference +// +// Created by Krzysztof Zablocki on 18.10.2017 +// Copyright © 2017 Krzysztof Zablocki. All rights reserved. +// + +import Foundation + +public typealias IndentationType = Difference.IndentationType + +public struct DifferenceNameLabels { + let expected: String + let received: String + let missing: String + let extra: String + + public init(expected: String, received: String, missing: String, extra: String) { + self.expected = expected + self.received = received + self.missing = missing + self.extra = extra + } + + public static var expectation: Self { + Self( + expected: "Expected", + received: "Received", + missing: "Missing", + extra: "Extra" + ) + } + + public static var comparing: Self { + Self( + expected: "Previous", + received: "Current", + missing: "Removed", + extra: "Added" + ) + } +} + +private struct Differ { + private let indentationType: IndentationType + private let skipPrintingOnDiffCount: Bool + private let nameLabels: DifferenceNameLabels + + init( + indentationType: IndentationType, + skipPrintingOnDiffCount: Bool, + nameLabels: DifferenceNameLabels + ) { + self.indentationType = indentationType + self.skipPrintingOnDiffCount = skipPrintingOnDiffCount + self.nameLabels = nameLabels + } + + func diff(_ expected: T, _ received: T) -> [String] { + let lines = diffLines(expected, received, level: 0) + return buildLineContents(lines: lines) + } + + func diffLines(_ expected: T, _ received: T, level: Int = 0) -> [Line] { + let expectedMirror = Mirror(reflecting: expected) + let receivedMirror = Mirror(reflecting: received) + + guard expectedMirror.children.count != 0, receivedMirror.children.count != 0 else { + let receivedDump = String(dumping: received) + if receivedDump != String(dumping: expected) { + return handleChildless(expected, expectedMirror, received, receivedMirror, level) + } else if expectedMirror.displayStyle == .enum, receivedDump.hasPrefix("__C.") { // enum and C bridged + let expectedValue = enumIntValue(for: expected) + let receivedValue = enumIntValue(for: received) + if expectedValue != receivedValue { + return handleChildless(expectedValue, expectedMirror, receivedValue, receivedMirror, level) + } + } + return [] + } + + // Remove embedding of `some` for optional types, as it offers no value + guard expectedMirror.displayStyle != .optional else { + if let expectedUnwrapped = expectedMirror.firstChildenValue, let receivedUnwrapped = receivedMirror.firstChildenValue { + return diffLines(expectedUnwrapped, receivedUnwrapped, level: level) + } + return [] + } + + let hasDiffNumOfChildren = expectedMirror.children.count != receivedMirror.children.count + switch (expectedMirror.displayStyle, receivedMirror.displayStyle) { + case (.collection?, .collection?) where hasDiffNumOfChildren, + (.dictionary?, .dictionary?) where hasDiffNumOfChildren, + (.set?, .set?) where hasDiffNumOfChildren, + (.enum?, .enum?) where hasDiffNumOfChildren: + return [generateDifferentCountBlock(expected, expectedMirror, received, receivedMirror, level)] + case (.dictionary?, .dictionary?): + if let expectedDict = expected as? Dictionary, + let receivedDict = received as? Dictionary { + var resultLines: [Line] = [] + let missingKeys = Set(expectedDict.keys).subtracting(receivedDict.keys) + let extraKeys = Set(receivedDict.keys).subtracting(expectedDict.keys) + let commonKeys = Set(receivedDict.keys).intersection(expectedDict.keys) + commonKeys.forEach { key in + let results = diffLines(expectedDict[key], receivedDict[key], level: level + 1) + if !results.isEmpty { + resultLines.append(Line(contents: "Key \(key.description):", indentationLevel: level, canBeOrdered: true, children: results)) + } + } + if (!missingKeys.isEmpty) { + var missingKeyPairs: [Line] = [] + missingKeys.forEach { key in + missingKeyPairs.append(Line(contents: "\(key.description): \(String(describing: expectedDict[key]))", indentationLevel: level + 1, canBeOrdered: true)) + } + resultLines.append(Line(contents: "\(nameLabels.missing) key pairs:", indentationLevel: level, canBeOrdered: false, children: missingKeyPairs)) + } + if (!extraKeys.isEmpty) { + var extraKeyPairs: [Line] = [] + extraKeys.forEach { key in + extraKeyPairs.append(Line(contents: "\(key.description): \(String(describing: receivedDict[key]))", indentationLevel: level + 1, canBeOrdered: true)) + } + resultLines.append(Line(contents: "\(nameLabels.extra) key pairs:", indentationLevel: level, canBeOrdered: false, children: extraKeyPairs)) + } + return resultLines + } + case (.set?, .set?): + if let expectedSet = expected as? Set, + let receivedSet = received as? Set { + let missing = expectedSet.subtracting(receivedSet) + .map { unique in + Line(contents: "\(nameLabels.missing): \(unique.description)", indentationLevel: level, canBeOrdered: true) + } + let extras = receivedSet.subtracting(expectedSet) + .map { unique in + Line(contents: "\(nameLabels.extra): \(unique.description)", indentationLevel: level, canBeOrdered: true) + } + return missing + extras + } + // Handles different enum cases that have children to prevent printing entire object + case (.enum?, .enum?) where expectedMirror.children.first?.label != receivedMirror.children.first?.label: + let expectedPrintable = enumLabelFromFirstChild(expectedMirror) ?? "UNKNOWN" + let receivedPrintable = enumLabelFromFirstChild(receivedMirror) ?? "UNKNOWN" + return generateExpectedReceiveLines(expectedPrintable, receivedPrintable, level) + default: + break + } + + var resultLines = [Line]() + let zipped = zip(expectedMirror.children, receivedMirror.children) + zipped.enumerated().forEach { (index, zippedValues) in + let lhs = zippedValues.0 + let rhs = zippedValues.1 + let childName = "\(expectedMirror.displayStyleDescriptor(index: index))\(lhs.label ?? ""):" + let results = diffLines(lhs.value, rhs.value, level: level + 1) + + if !results.isEmpty { + let line = Line(contents: childName, + indentationLevel: level, + canBeOrdered: true, + children: results + ) + resultLines.append(line) + } + } + return resultLines + } + + fileprivate func handleChildless( + _ expected: T, + _ expectedMirror: Mirror, + _ received: T, + _ receivedMirror: Mirror, + _ indentationLevel: Int + ) -> [Line] { + // Empty collections are "childless", so we may need to generate a different count block instead of treating as a + // childless enum. + guard !expectedMirror.canBeEmpty else { + return [generateDifferentCountBlock(expected, expectedMirror, received, receivedMirror, indentationLevel)] + } + + let receivedPrintable: String + let expectedPrintable: String + // Received mirror has a different number of arguments to expected + if receivedMirror.children.count == 0, expectedMirror.children.count != 0 { + // Print whole description of received, as it's only a label if childless + receivedPrintable = String(dumping: received) + // Get the label from the expected, to prevent printing long list of arguments + expectedPrintable = enumLabelFromFirstChild(expectedMirror) ?? String(describing: expected) + } else if expectedMirror.children.count == 0, receivedMirror.children.count != 0 { + receivedPrintable = enumLabelFromFirstChild(receivedMirror) ?? String(describing: received) + expectedPrintable = String(dumping: expected) + } else { + receivedPrintable = String(describing: received) + expectedPrintable = String(describing: expected) + } + return generateExpectedReceiveLines(expectedPrintable, receivedPrintable, indentationLevel) + } + + private func generateDifferentCountBlock( + _ expected: T, + _ expectedMirror: Mirror, + _ received: T, + _ receivedMirror: Mirror, + _ indentationLevel: Int + ) -> Line { + var expectedPrintable = "(\(expectedMirror.children.count))" + var receivedPrintable = "(\(receivedMirror.children.count))" + if !skipPrintingOnDiffCount { + expectedPrintable.append(" \(expected)") + receivedPrintable.append(" \(received)") + } + return Line( + contents: "Different count:", + indentationLevel: indentationLevel, + canBeOrdered: false, + children: generateExpectedReceiveLines(expectedPrintable, receivedPrintable, indentationLevel + 1) + ) + } + + private func generateExpectedReceiveLines( + _ expected: String, + _ received: String, + _ indentationLevel: Int + ) -> [Line] { + return [ + Line(contents: "\(nameLabels.received): \(received)", indentationLevel: indentationLevel, canBeOrdered: false), + Line(contents: "\(nameLabels.expected): \(expected)", indentationLevel: indentationLevel, canBeOrdered: false) + ] + } + + private func buildLineContents(lines: [Line]) -> [String] { + let linesContents = lines.map { line in line.generateContents(indentationType: indentationType) } + // In the case of this being a top level failure (e.g. both mirrors have no children, like comparing two + // primitives `diff(2,3)`, we only want to produce one failure to have proper spacing. + let isOnlyTopLevelFailure = lines.map { $0.hasChildren }.filter { $0 }.isEmpty + if isOnlyTopLevelFailure { + return [linesContents.joined()] + } else { + return linesContents + } + } + + /// Creates int value from Objective-C enum. + private func enumIntValue(for object: T) -> Int { + withUnsafePointer(to: object) { + $0.withMemoryRebound(to: Int.self, capacity: 1) { + $0.pointee + } + } + } +} + +public enum Difference { + /// Styling of the diff indentation. + /// `pipe` example: + /// address: + /// | street: + /// | | Received: 2nd Street + /// | | Expected: Times Square + /// | counter: + /// | | counter: + /// | | | Received: 1 + /// | | | Expected: 2 + /// `tab` example: + /// address: + /// street: + /// Received: 2nd Street + /// Expected: Times Square + /// counter: + /// counter: + /// Received: 1 + /// Expected: 2 + public enum IndentationType: String, CaseIterable { + case pipe = "|\t" + case tab = "\t" + } +} + +public struct Line { + public init(contents: String, indentationLevel: Int, children: [Line], canBeOrdered: Bool) { + self.contents = contents + self.indentationLevel = indentationLevel + self.children = children + self.canBeOrdered = canBeOrdered + } + + public let contents: String + public let indentationLevel: Int + public let children: [Line] + public let canBeOrdered: Bool + + public var hasChildren: Bool { !children.isEmpty } + + init( + contents: String, + indentationLevel: Int, + canBeOrdered: Bool, + children: [Line] = [] + ) { + self.contents = contents + self.indentationLevel = indentationLevel + self.children = children + self.canBeOrdered = canBeOrdered + } + + public func generateContents(indentationType: IndentationType) -> String { + let indentationString = indentation(level: indentationLevel, indentationType: indentationType) + let childrenContents = children + .sorted { lhs, rhs in + guard lhs.canBeOrdered && rhs.canBeOrdered else { return false } + return lhs.contents < rhs.contents + } + .map { $0.generateContents(indentationType: indentationType)} + .joined() + return "\(indentationString)\(contents)\n" + childrenContents + } + + private func indentation(level: Int, indentationType: IndentationType) -> String { + (0..(dumping object: T) { + self.init() + dump(object, to: &self) + self = withoutDumpArtifacts + } + + // Removes the artifacts of using dumping initialiser to improve readability + private var withoutDumpArtifacts: String { + self.replacingOccurrences(of: "- ", with: "") + .replacingOccurrences(of: "\n", with: "") + } +} + +// In the case of an enum with an argument being compared to a different enum case, +// pull the case name from the mirror +private func enumLabelFromFirstChild(_ mirror: Mirror) -> String? { + switch mirror.displayStyle { + case .enum: return mirror.children.first?.label + default: return nil + } +} + +fileprivate extension Mirror { + func displayStyleDescriptor(index: Int) -> String { + switch self.displayStyle { + case .enum: return "Enum " + case .collection: return "Collection[\(index)]" + default: return "" + } + } + + // Used to show "different count" message if mirror has no children, + // as some displayStyles can have 0 children. + var canBeEmpty: Bool { + switch self.displayStyle { + case .collection, + .dictionary, + .set: + return true + default: + return false + } + } + + var firstChildenValue: Any? { + children.first?.value + } +} + +/// Builds list of differences between 2 objects +/// +/// - Parameters: +/// - expected: Expected value +/// - received: Received value +/// - indentationType: Style of indentation to use +/// - skipPrintingOnDiffCount: Skips the printing of the object when a collection has a different count +/// +/// - Returns: List of differences +public func diff( + _ expected: T, + _ received: T, + indentationType: Difference.IndentationType = .pipe, + skipPrintingOnDiffCount: Bool = false, + nameLabels: DifferenceNameLabels = .expectation +) -> [String] { + Differ(indentationType: indentationType, skipPrintingOnDiffCount: skipPrintingOnDiffCount, nameLabels: nameLabels) + .diff(expected, received) +} + +/// Builds list of differences between 2 objects +/// +/// - Parameters: +/// - expected: Expected value +/// - received: Received value +/// - indentationType: Style of indentation to use +/// - skipPrintingOnDiffCount: Skips the printing of the object when a collection has a different count +/// +/// - Returns: List of differences +public func diffLines( + _ expected: T, + _ received: T, + indentationType: Difference.IndentationType = .pipe, + skipPrintingOnDiffCount: Bool = false, + nameLabels: DifferenceNameLabels = .expectation +) -> [Line] { + Differ(indentationType: indentationType, skipPrintingOnDiffCount: skipPrintingOnDiffCount, nameLabels: nameLabels) + .diffLines(expected, received) +} + +/// Prints list of differences between 2 objects +/// +/// - Parameters: +/// - expected: Expected value +/// - received: Received value +/// - indentationType: Style of indentation to use +/// - skipPrintingOnDiffCount: Skips the printing of the object when a collection has a different count +public func dumpDiff( + _ expected: T, + _ received: T, + indentationType: Difference.IndentationType = .pipe, + skipPrintingOnDiffCount: Bool = false +) { + // skip equal + guard expected != received else { + return + } + + diff( + expected, + received, + indentationType: indentationType, + skipPrintingOnDiffCount: skipPrintingOnDiffCount + ).forEach { print($0) } +} + + +/// Prints list of differences between 2 objects +/// +/// - Parameters: +/// - expected: Expected value +/// - received: Received value +/// - indentationType: Style of indentation to use +/// - skipPrintingOnDiffCount: Skips the printing of the object when a collection has a different count +public func dumpDiff( + _ expected: T, + _ received: T, + indentationType: Difference.IndentationType = .pipe, + skipPrintingOnDiffCount: Bool = false +) { + diff( + expected, + received, + indentationType: indentationType, + skipPrintingOnDiffCount: skipPrintingOnDiffCount + ).forEach { print($0) } +} diff --git a/TestTools/StreamChatTestTools/Extensions/XCTest+Helpers.swift b/TestTools/StreamChatTestTools/Extensions/XCTest+Helpers.swift new file mode 100644 index 00000000000..93a161013c5 --- /dev/null +++ b/TestTools/StreamChatTestTools/Extensions/XCTest+Helpers.swift @@ -0,0 +1,335 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import Foundation +import XCTest + +/// Asserts diff between the expected and received values +public func XCTAssertEqual(_ expected: T, + _ received: T, + file: StaticString = #filePath, + line: UInt = #line) { + if TestRunnerEnvironment.isCI { + // Use built-in `XCTAssertEqual` when running on the CI to get CI-friendly logs. + XCTAssertEqual(received, expected, "", file: file, line: line) + } else { + XCTAssertTrue( + expected == received, + "Found difference for \n" + diff(expected, received).joined(separator: ", "), + file: file, + line: line + ) + } +} + +// MARK: Errors + +/// Asserts when two given errors are not equal +/// +/// Usage: +/// XCTAssertEqual(error, .fileSizeTooLarge(messageId: messageId)) +public func XCTAssertEqual(_ error1: T, + _ error: T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line, + usesDifference: Bool = true) { + XCTAssertEqual(error1.stringReflection, + error.stringReflection, + diffMessage(error1, received: error, message: message()), + file: file, + line: line) +} + +/// Asserts when two (optional) errors are not equal. +/// +/// Usage: +/// XCTAssertEqual(some?.error, .fileSizeTooLarge(messageId: messageId)) +public func XCTAssertEqual(_ error1: T?, + _ error: T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line) { + XCTAssertEqual(error1?.stringReflection, + error.stringReflection, + diffMessage(error1, received: error, message: message()), + file: file, + line: line) +} + +/// Asserts when two given errors are not equal +/// +/// Usage: +/// XCTAssertEqual(error, .fileSizeTooLarge(messageId: messageId)) +public func XCTAssertEqual(_ error1: T, + _ error: T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line) where T: Equatable { + if error == error1 { + /// This covers the case, when `Equatable` conformance of the `Error` was overriden by the custom implementation + XCTAssertTrue(true, message()) + return + } + + XCTAssertEqual(error1.stringReflection, + error.stringReflection, + diffMessage(error1, received: error, message: message()), + file: file, + line: line) +} + +/// Asserts when two (optional) errors are not equal. +/// +/// Usage: +/// XCTAssertEqual(some?.error, .fileSizeTooLarge(messageId: messageId)) +public func XCTAssertEqual(_ error1: T?, + _ error: T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line) where T: Equatable { + if let equalError = error1, + equalError == error { + /// This covers the case, when `Equatable` conformance of the `Error` was overriden by the custom implementation + XCTAssertTrue(true, message()) + return + } + + XCTAssertEqual(error1?.stringReflection, + error.stringReflection, + diffMessage(error1, received: error, message: message()), + file: file, + line: line) +} + +// MARK: Throws + +/// Asserts when given expression throws an error. +/// +/// - Parameters: +/// - expression: An expression that can throw +/// - message: An description message for failure +/// - errorHandler: An error handler to access the error with concrete type +public func XCTAssertThrowsError(_ expression: @autoclosure () throws -> T, + _ message: String, + file: StaticString = #filePath, + line: UInt = #line, + _ errorHandler: (U) -> Void) { + XCTAssertThrowsError(try expression(), message, file: file, line: line) { (error) in + guard let typedError = error as? U else { + XCTFail("Error: \(error) doesnt match with given error type: \(U.self)", + file: file, + line: line) + return + } + errorHandler(typedError) + } +} + +/// Asserts when thrown error type doesnt match given type +public func XCTAssertThrowsError(ofType: U.Type, + _ expression: @autoclosure () throws -> T, + _ message: String, + file: StaticString = #filePath, + line: UInt = #line, + _ errorHandler: (U) -> Void) { + XCTAssertThrowsError(try expression(), + message, + file: file, + line: line, + errorHandler) +} + +/// Asserts when given throwing expression doesnt throw the expected error +/// +/// - Parameters: +/// - expression: throw expression +/// - error: Awaited error +/// +/// Usage: +/// XCTAssertThrowsError(try PathUpdate(with: data), ParsingError.failedToParseJSON) +public func XCTAssertThrowsError(_ expression: @autoclosure () throws -> T, + _ error: Error, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line) { + XCTAssertThrowsError(try expression(), message()) { (thrownError) in + XCTAssertEqual(thrownError, + error, + diffMessage(thrownError, received: error, message: message()), + file: file, + line: line) + } +} + +// MARK: Result + +/// Asserts when given failure result `Result` doesn't match the given error +/// +/// - Parameters: +/// - result: Result of type `Result` +/// - value: Awaited success value, that is equatable +/// +/// Usage: +/// XCTAssertEqual(result, success: "Success") +public func XCTAssertEqual(_ result: Result, + success value: Value, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line) { + let resultValue = XCTAssertResultSuccess(result, + message(), + file: file, + line: line) + XCTAssertEqual(resultValue, value, diffMessage(resultValue, received: value, message: message()), file: file, line: line) +} + +/// Asserts when given failure result `Result` doesn't match the given error +/// +/// - Parameters: +/// - result: Result of type `Result` +/// - error: Awaited error +/// +/// Usage: +/// XCTAssertEqual(result, failure: .noMachineId) +public func XCTAssertEqual(_ result: Result, + failure error: ErrorType, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line) { + let errorMessage = message() + XCTAssertResultFailure(result, + errorMessage, + file: file, + line: line) { failureError in + XCTAssertEqual(failureError, + error, + diffMessage(failureError, received: error, message: errorMessage), + file: file, + line: line) + } +} + +/// Asserts when given failure result `Result` doesn't match the given `equatable` error +/// +/// - Parameters: +/// - result: Result of type `Result` +/// - error: Awaited error +/// +/// Usage: +/// XCTAssertEqual(result, failure: .noMachineId) +public func XCTAssertEqual(_ result: Result, + failure error: ErrorType, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line) where ErrorType: Equatable { + let errorMessage = message() + XCTAssertResultFailure(result, + errorMessage, + file: file, + line: line) { failureError in + XCTAssertEqual(failureError, + error, + diffMessage(failureError, received: error, message: errorMessage), + file: file, + line: line) + } +} + +/// Asserts when given result `Result` is failure +@discardableResult +public func XCTAssertResultSuccess(_ result: Result, + _ message: @autoclosure () -> String = "Expectation failed for result", + file: StaticString = #filePath, + line: UInt = #line) -> Value? { + switch result { + case .success(let value): + return value + case .failure: + XCTFail(message(), file: file, line: line) + return nil + } +} + +/// Asserts when given result `Result` has succeeded +/// +/// - Parameters: +/// - result: Result of type `Result` +/// - errorHandler: This closure gives you possibility to check the error of `ErrorType` in type-safe manner +/// +/// Usage: +/// XCTAssertResultFailure(result) { (error) in +/// XCTAssertEqual(error, .fileSizeTooLarge(messageId: messageId)) +/// } +public func XCTAssertResultFailure(_ result: Result, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line, + errorHandler: ((ErrorType) -> Void)? = nil) { + XCTAssertResultFailure(result, + ofErrorType: ErrorType.self, + message(), + file: file, + line: line, + errorHandler: errorHandler) +} + +/// Asserts when given result `Result` has succeeded or the result doesnt match the given `errorType` +/// +/// - Parameters: +/// - result: Result of type `Result` +/// - errorType: Awaited error type of error propagated through result's failure case +/// - errorHandler: This closure gives you possibility to check the error of `ErrorType` in type-safe manner +/// +/// Usage: +/// XCTAssertResultFailure(result) { (error) in +/// XCTAssertEqual(error, .fileSizeTooLarge(messageId: messageId)) +/// } +public func XCTAssertResultFailure(_ result: Result, + ofErrorType errorType: ErrorType.Type? = nil, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line, + errorHandler: ((ErrorType) -> Void)? = nil) { + switch result { + case .success: + XCTFail("Result was not Failure", + file: file, + line: line) + case .failure(let failureError): + guard + let errorType = errorType, + let errorHandler = errorHandler else { + return + } + + guard let error = failureError as? ErrorType else { + XCTFail("Result error: \(failureError) doesnt match with given error type: \(errorType)", + file: file, + line: line) + return + } + + errorHandler(error) + return // then fallthrough to fulfill expectations + } +} + +/// Errors are compared through string reflection +public extension Error { + var stringReflection: String { + String(reflecting: self) + } +} + +// MARK: Diff message + +func diffMessage(_ expected: Value, + received: Value, + message: @autoclosure () -> String = "") -> String { + [message(), + "Found difference for", + diff(expected, received).joined(separator: ", ") + ].joined(separator: "\n") +} diff --git a/TestTools/StreamChatTestTools/Extensions/XCTestCase+MockData.swift b/TestTools/StreamChatTestTools/Extensions/XCTestCase+MockData.swift index bc59bd6c9c5..5872494f5b2 100644 --- a/TestTools/StreamChatTestTools/Extensions/XCTestCase+MockData.swift +++ b/TestTools/StreamChatTestTools/Extensions/XCTestCase+MockData.swift @@ -6,6 +6,12 @@ import XCTest extension XCTestCase { public static func mockData(fromJSONFile name: String) -> Data { - XCTestCase.mockData(fromFile: "\(Bundle.testTools.pathToJSONsFolder)\(name)", bundle: .testTools) + let file = "\(Bundle.testTools.pathToJSONsFolder)\(name)" + guard let url = Bundle.testTools.url(forResource: file, withExtension: "json") else { + XCTFail("\n❌ Mock file \"\(file).json\" not found in bundle \(Bundle.testTools.bundleURL.lastPathComponent)") + return .init() + } + + return try! Data(contentsOf: url) } } diff --git a/TestTools/StreamChatTestTools/Extensions/XCTestCase+StressTest.swift b/TestTools/StreamChatTestTools/Extensions/XCTestCase+StressTest.swift new file mode 100644 index 00000000000..c7be6163184 --- /dev/null +++ b/TestTools/StreamChatTestTools/Extensions/XCTestCase+StressTest.swift @@ -0,0 +1,25 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import XCTest + +/// Base class for stress tests +/// +/// - runs 100 times if `TestRunnerEnvironment.isStressTest` +/// - by default ends test when test failure occurs +open class StressTestCase: XCTestCase { + override open func setUp() { + super.setUp() + + continueAfterFailure = false + } + + override open func invokeTest() { + for _ in 0.. { + public var calls: [In] = [] + public var result: (In) -> Out = { _ in fatalError() } + + public init() {} + + public init(result: @escaping (In) -> Out) { + self.result = result + } + + public var count: Int { + calls.count + } + + public var called: Bool { + !calls.isEmpty + } + + public static func mock(for: (In) throws -> Out) -> MockFunc { + MockFunc() + } + + public func call(with input: In) { + calls.append(input) + } + + public var input: In { + calls[count - 1] + } + + public var output: Out { + result(input) + } + + public func callAndReturn(_ input: In) -> Out { + call(with: input) + return output + } +} + +extension MockFunc { + public func returns(_ value: Out) { + result = { _ in value } + } + + public func succeeds(_ value: T) + where Out == Result { + result = { _ in .success(value) } + } + + public func fails(_ error: Error) + where Out == Result { + result = { _ in .failure(error) } + } +} diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/AuthenticationRepository_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/AuthenticationRepository_Mock.swift index d384f85f8a3..6abd4113a79 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/AuthenticationRepository_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/AuthenticationRepository_Mock.swift @@ -4,7 +4,6 @@ import Foundation @testable import StreamChat -import StreamSwiftTestHelpers class AuthenticationRepository_Mock: AuthenticationRepository, Spy { enum Signature { diff --git a/TestTools/StreamChatTestTools/SpyPattern/QueueAware/QueueAwareDelegate.swift b/TestTools/StreamChatTestTools/SpyPattern/QueueAware/QueueAwareDelegate.swift new file mode 100644 index 00000000000..e539c0b6188 --- /dev/null +++ b/TestTools/StreamChatTestTools/SpyPattern/QueueAware/QueueAwareDelegate.swift @@ -0,0 +1,37 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import XCTest + +/// A delegate superclass which can verify a certain method was called on the given queue. +/// +/// Example usage: +/// ``` +/// class TestMyDelegate: QueueAwareDelegate { +/// func controllerWillStartFetchingRemoteData(_ controller: Controller) { +/// validateQueue() +/// } +/// } +/// ``` +open class QueueAwareDelegate { + // Checks the delegate was called on the correct queue + public let expectedQueueId: UUID + public let file: StaticString + public let line: UInt + + public init(expectedQueueId: UUID, file: StaticString = #filePath, line: UInt = #line) { + self.expectedQueueId = expectedQueueId + self.file = file + self.line = line + } + + public func validateQueue(function: StaticString = #function) { + XCTAssertTrue( + DispatchQueue.isTestQueue(withId: expectedQueueId), + "Delegate method \(function) called on an incorrect queue", + file: file, + line: line + ) + } +} diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/Spy.swift new file mode 100644 index 00000000000..51a2dc1d9d6 --- /dev/null +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/Spy.swift @@ -0,0 +1,60 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import Foundation +import XCTest + +public protocol Spy: AnyObject { + var recordedFunctions: [String] { get set } +} + +public extension Spy { + func clear() { + recordedFunctions.removeAll() + } + + func record(function: String = #function) { + recordedFunctions.append(function) + } + + func numberOfCalls(on function: String) -> Int { + recordedFunctions.reduce(0) { $0 + ($1 == function ? 1 : 0) } + } +} + +extension String { + public func wasCalled(on spy: Spy, times: Int? = nil) -> Bool { + let function = self + let wasCalled = spy.recordedFunctions.contains(function) + + guard wasCalled, let times = times else { + return wasCalled + } + + let callCount = spy.numberOfCalls(on: function) + return callCount == times + } + + public func wasNotCalled(on spy: Spy) -> Bool { + !wasCalled(on: spy) + } +} + +public func XCTAssertCall(_ function: String, on spy: Spy, times: Int? = nil, file: StaticString = #filePath, line: UInt = #line) { + if function.wasCalled(on: spy, times: times) { + XCTAssertTrue(true, file: file, line: line) + return + } + + XCTFail("\(function) was called \(spy.numberOfCalls(on: function)) times", file: file, line: line) +} + +public func XCTAssertNotCall(_ function: String, on spy: Spy, file: StaticString = #filePath, line: UInt = #line) { + if function.wasNotCalled(on: spy) { + XCTAssertTrue(true, file: file, line: line) + return + } + + XCTFail("\(function) was called \(spy.numberOfCalls(on: function)) times", file: file, line: line) +} diff --git a/TestTools/StreamChatTestTools/StreamChatTestTools.swift b/TestTools/StreamChatTestTools/StreamChatTestTools.swift index 2128252a00d..149f34803b3 100644 --- a/TestTools/StreamChatTestTools/StreamChatTestTools.swift +++ b/TestTools/StreamChatTestTools/StreamChatTestTools.swift @@ -3,9 +3,17 @@ // import Foundation -@_exported import StreamSwiftTestHelpers import XCTest +/// The default timeout value used by the `willBe___` family of assertions. +public let defaultTimeout: TimeInterval = TestRunnerEnvironment.isCI || TestRunnerEnvironment.isStressTest ? 10 : 1 + +/// The default timeout value used by the `stays___` family of assertions. +public let defaultTimeoutForInversedExpecations: TimeInterval = TestRunnerEnvironment.isCI || TestRunnerEnvironment.isStressTest ? 1 : 0.1 + +/// How big is the period between expression evaluations. +public let evaluationPeriod: TimeInterval = 0.00001 + extension Bundle { private final class StreamChatTestTools {} diff --git a/TestTools/StreamChatTestTools/TestRunnerEnvironment.swift b/TestTools/StreamChatTestTools/TestRunnerEnvironment.swift new file mode 100644 index 00000000000..c560f26abd2 --- /dev/null +++ b/TestTools/StreamChatTestTools/TestRunnerEnvironment.swift @@ -0,0 +1,21 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import Foundation + +public enum TestRunnerEnvironment { + /// `true` if the tests are currently running on the CI. We get this information by checking for the custom `CI` environment + /// variable passed in to the process. + public static var isCI: Bool { + ProcessInfo.processInfo.environment["CI"] == "TRUE" + } + + /// Number of stress test invocations + public static var testInvocations: Int { + ProcessInfo.processInfo.environment["TEST_INVOCATIONS"].flatMap(Int.init) ?? 1 + } + + /// `true` if we invoke stress tests more than a single time + public static var isStressTest: Bool { testInvocations > 1 } +} diff --git a/TestTools/StreamChatTestTools/Wait/WaitFor.swift b/TestTools/StreamChatTestTools/Wait/WaitFor.swift new file mode 100644 index 00000000000..9bd850b186e --- /dev/null +++ b/TestTools/StreamChatTestTools/Wait/WaitFor.swift @@ -0,0 +1,59 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import XCTest + +public enum WaiterError: Error { + case waitingForResultTimedOut +} + +/// The maximum time `waitFor` waits for the wrapped function to complete. When running stress tests, this value +/// is much higher because the system might be under very heavy load. +public let waitForTimeout: TimeInterval = TestRunnerEnvironment.isStressTest || TestRunnerEnvironment.isCI ? 10 : 1 + +/// Allows calling an asynchronous function in the synchronous way in tests. +/// +/// Example usage: +/// ``` +/// func asyncFunction(completion: (T) -> Void) { } +/// let result: T = try waitFor { asyncFunction(completion: $0) } +/// ``` +/// +/// - Parameters: +/// - timeout: The maximum time this function waits for `action` to complete. +/// - action: The asynchronous action this function wrapps. +/// - done: `action` is required to call this closure when finished. The value then becomes the return value of +/// the whole `waitFor` function. +/// +/// - Throws: `WaiterError.waitingForResultTimedOut` if `action` doesn't call the completion closure within the `timeout` period. +/// +/// - Returns: The result of `action`. +public func waitFor( + timeout: TimeInterval = waitForTimeout, + file: StaticString = #filePath, + line: UInt = #line, + _ action: (_ done: @escaping (T) -> Void) -> Void +) throws -> T { + let expectation = XCTestExpectation(description: "Action completed") + var result: T? + action { resultValue in + result = resultValue + if Thread.isMainThread { + expectation.fulfill() + } else { + DispatchQueue.main.async { + expectation.fulfill() + } + } + } + + let waiterResult = XCTWaiter.wait(for: [expectation], timeout: timeout) + switch waiterResult { + case .completed where result != nil: + return result! + default: + XCTFail("Waiting for the result timed out after \(timeout)", file: file, line: line) + throw WaiterError.waitingForResultTimedOut + } +} diff --git a/TestTools/StreamChatTestTools/Wait/WaitUntil.swift b/TestTools/StreamChatTestTools/Wait/WaitUntil.swift new file mode 100644 index 00000000000..7d4e745e94a --- /dev/null +++ b/TestTools/StreamChatTestTools/Wait/WaitUntil.swift @@ -0,0 +1,22 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +import Foundation +import XCTest + +extension XCTestCase { + public func waitUntil(timeout: TimeInterval = 0.5, _ action: (_ done: @escaping () -> Void) -> Void) { + let expectation = XCTestExpectation(description: "Action completed") + action { + if Thread.isMainThread { + expectation.fulfill() + } else { + DispatchQueue.main.async { + expectation.fulfill() + } + } + } + wait(for: [expectation], timeout: timeout) + } +} diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/CreateCallPayload_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/CreateCallPayload_Tests.swift index 023f4a2dbe4..6558a82a6b6 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/CreateCallPayload_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/CreateCallPayload_Tests.swift @@ -4,7 +4,7 @@ import Foundation @testable import StreamChat -import StreamSwiftTestHelpers +import StreamChatTestTools import XCTest final class CreateCallPayload_Tests: XCTestCase { diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/MessagePayloads_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/MessagePayloads_Tests.swift index dde40226f97..3dca85094f3 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/MessagePayloads_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/MessagePayloads_Tests.swift @@ -8,10 +8,7 @@ import XCTest final class MessagePayload_Tests: XCTestCase { let messageJSON = XCTestCase.mockData(fromJSONFile: "Message") - let messageJSONWithCorruptedAttachments = XCTestCase.mockData( - fromFile: "MessageWithBrokenAttachments", - bundle: .testTools - ) + let messageJSONWithCorruptedAttachments = XCTestCase.mockData(fromJSONFile: "MessageWithBrokenAttachments") let messageCustomData: [String: RawJSON] = ["secret_note": .string("Anakin is Vader!")] func test_messagePayload_isSerialized_withDefaultExtraData() throws { diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/UserPayloads_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/UserPayloads_Tests.swift index 287d0ef14d2..e6c52eed833 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/UserPayloads_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/UserPayloads_Tests.swift @@ -151,10 +151,7 @@ final class UserUpdateResponse_Tests: XCTestCase { } func test_currentUserUpdateResponseJSON_whenMissingUser_failsSerialization() { - let currentUserUpdateResponseJSON = XCTestCase.mockData( - fromFile: "UserUpdateResponse+MissingUser", - bundle: .testTools - ) + let currentUserUpdateResponseJSON = XCTestCase.mockData(fromJSONFile: "UserUpdateResponse+MissingUser") XCTAssertThrowsError(try JSONDecoder.default.decode( UserUpdateResponse.self, from: currentUserUpdateResponseJSON )) diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Requests/CallRequestBody_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Requests/CallRequestBody_Tests.swift index 1f9643f6ece..a00fa7fabe6 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/Requests/CallRequestBody_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/Requests/CallRequestBody_Tests.swift @@ -4,7 +4,7 @@ import Foundation @testable import StreamChat -import StreamSwiftTestHelpers +import StreamChatTestTools import XCTest final class CallRequestBody_Tests: XCTestCase { diff --git a/Tests/StreamChatTests/Audio/StreamAppStateObserver_Tests.swift b/Tests/StreamChatTests/Audio/StreamAppStateObserver_Tests.swift index dc8339a5026..e0019e3bcb5 100644 --- a/Tests/StreamChatTests/Audio/StreamAppStateObserver_Tests.swift +++ b/Tests/StreamChatTests/Audio/StreamAppStateObserver_Tests.swift @@ -4,6 +4,7 @@ import Foundation @testable import StreamChat +import StreamChatTestTools import UIKit import XCTest diff --git a/Tests/StreamChatTests/Database/DTOs/AttachmentDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/AttachmentDTO_Tests.swift index 79d017956bd..82dd0dfc6b0 100644 --- a/Tests/StreamChatTests/Database/DTOs/AttachmentDTO_Tests.swift +++ b/Tests/StreamChatTests/Database/DTOs/AttachmentDTO_Tests.swift @@ -90,10 +90,7 @@ final class AttachmentDTO_Tests: XCTestCase { let cid: ChannelId = .unique let messageId: MessageId = .unique - let giphyWithoutActionsJSON = XCTestCase.mockData( - fromFile: "AttachmentPayloadGiphyWithoutActions", - bundle: .testTools - ) + let giphyWithoutActionsJSON = XCTestCase.mockData(fromJSONFile: "AttachmentPayloadGiphyWithoutActions") let attachment = try JSONDecoder.default.decode(MessageAttachmentPayload.self, from: giphyWithoutActionsJSON) let attachmentId = AttachmentId(cid: cid, messageId: messageId, index: 0) diff --git a/Tests/StreamChatTests/Models/Attachments/AnyAttachmentUpdater_Tests.swift b/Tests/StreamChatTests/Models/Attachments/AnyAttachmentUpdater_Tests.swift index 8de3b928622..3a6cec1daf7 100644 --- a/Tests/StreamChatTests/Models/Attachments/AnyAttachmentUpdater_Tests.swift +++ b/Tests/StreamChatTests/Models/Attachments/AnyAttachmentUpdater_Tests.swift @@ -3,6 +3,7 @@ // @testable import StreamChat +import StreamChatTestTools import XCTest final class AnyAttachmentUpdater_Tests: XCTestCase { diff --git a/Tests/StreamChatTests/Models/Attachments/GiphyAttachmentPayload_Tests.swift b/Tests/StreamChatTests/Models/Attachments/GiphyAttachmentPayload_Tests.swift index 00db14c5db5..7a40e627db7 100644 --- a/Tests/StreamChatTests/Models/Attachments/GiphyAttachmentPayload_Tests.swift +++ b/Tests/StreamChatTests/Models/Attachments/GiphyAttachmentPayload_Tests.swift @@ -3,6 +3,7 @@ // @testable import StreamChat +import StreamChatTestTools import XCTest final class GiphyAttachmentPayload_Tests: XCTestCase { diff --git a/Tests/StreamChatTests/Query/MessageSearchQuery_Tests.swift b/Tests/StreamChatTests/Query/MessageSearchQuery_Tests.swift index d2689fee62e..625ed107b0b 100644 --- a/Tests/StreamChatTests/Query/MessageSearchQuery_Tests.swift +++ b/Tests/StreamChatTests/Query/MessageSearchQuery_Tests.swift @@ -3,6 +3,7 @@ // @testable import StreamChat +import StreamChatTestTools import XCTest final class MessageSearchFilterScope_Tests: StressTestCase { diff --git a/Tests/StreamChatTests/StreamChatStressTests/Atomic_StressTests.swift b/Tests/StreamChatTests/StreamChatStressTests/Atomic_StressTests.swift index 63091ab18b8..a42db8d725b 100644 --- a/Tests/StreamChatTests/StreamChatStressTests/Atomic_StressTests.swift +++ b/Tests/StreamChatTests/StreamChatStressTests/Atomic_StressTests.swift @@ -3,6 +3,7 @@ // @testable import StreamChat +import StreamChatTestTools import XCTest final class Atomic_Tests: StressTestCase { diff --git a/Tests/StreamChatTests/StreamChatStressTests/Cached_StressTests.swift b/Tests/StreamChatTests/StreamChatStressTests/Cached_StressTests.swift index 1cbf979a93d..e2ef2ec1959 100644 --- a/Tests/StreamChatTests/StreamChatStressTests/Cached_StressTests.swift +++ b/Tests/StreamChatTests/StreamChatStressTests/Cached_StressTests.swift @@ -3,6 +3,7 @@ // @testable import StreamChat +import StreamChatTestTools import XCTest final class Cached_Tests: StressTestCase { diff --git a/Tests/StreamChatTests/StreamChatTests.swift b/Tests/StreamChatTests/StreamChatTests.swift index 1c2e1911adc..b9868a60a04 100644 --- a/Tests/StreamChatTests/StreamChatTests.swift +++ b/Tests/StreamChatTests/StreamChatTests.swift @@ -4,6 +4,4 @@ import Foundation -@_exported import StreamSwiftTestHelpers - final class StreamChatTests {} diff --git a/Tests/StreamChatTests/Utils/CooldownTracker_Tests.swift b/Tests/StreamChatTests/Utils/CooldownTracker_Tests.swift index 717ee9565a5..0b9688cdc3f 100644 --- a/Tests/StreamChatTests/Utils/CooldownTracker_Tests.swift +++ b/Tests/StreamChatTests/Utils/CooldownTracker_Tests.swift @@ -4,7 +4,6 @@ @testable import StreamChat @testable import StreamChatTestTools -import StreamSwiftTestHelpers import XCTest final class CooldownTracker_Tests: XCTestCase { diff --git a/Tests/StreamChatTests/Utils/Debouncer_Tests.swift b/Tests/StreamChatTests/Utils/Debouncer_Tests.swift index a3c64327277..380884be8d9 100644 --- a/Tests/StreamChatTests/Utils/Debouncer_Tests.swift +++ b/Tests/StreamChatTests/Utils/Debouncer_Tests.swift @@ -5,7 +5,6 @@ import Foundation @testable import StreamChat @testable import StreamChatTestTools -import StreamSwiftTestHelpers import XCTest final class Debouncer_Tests: XCTestCase { diff --git a/Tests/StreamChatTests/Utils/MulticastDelegate_Tests.swift b/Tests/StreamChatTests/Utils/MulticastDelegate_Tests.swift index dbee48897d5..29bc26334ec 100644 --- a/Tests/StreamChatTests/Utils/MulticastDelegate_Tests.swift +++ b/Tests/StreamChatTests/Utils/MulticastDelegate_Tests.swift @@ -3,7 +3,7 @@ // @testable import StreamChat -import StreamSwiftTestHelpers +import StreamChatTestTools import XCTest final class MulticastDelegate_Tests: XCTestCase { diff --git a/Tests/StreamChatTests/Utils/Operations/AsyncOperation_Tests.swift b/Tests/StreamChatTests/Utils/Operations/AsyncOperation_Tests.swift index 523b05f7e20..08c42379050 100644 --- a/Tests/StreamChatTests/Utils/Operations/AsyncOperation_Tests.swift +++ b/Tests/StreamChatTests/Utils/Operations/AsyncOperation_Tests.swift @@ -3,6 +3,7 @@ // @testable import StreamChat +import StreamChatTestTools import XCTest final class AsyncOperation_Tests: XCTestCase { diff --git a/Tests/StreamChatUITests/SnapshotTests/VoiceRecording/Components/AudioQueuePlayerNextItemProvider_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/VoiceRecording/Components/AudioQueuePlayerNextItemProvider_Tests.swift index 92b114cc68d..230f5432fbe 100644 --- a/Tests/StreamChatUITests/SnapshotTests/VoiceRecording/Components/AudioQueuePlayerNextItemProvider_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/VoiceRecording/Components/AudioQueuePlayerNextItemProvider_Tests.swift @@ -3,8 +3,8 @@ // @testable import StreamChat +@testable import StreamChatTestTools @testable import StreamChatUI -import StreamSwiftTestHelpers import XCTest final class AudioQueuePlayerNextItemProvider_Tests: XCTestCase { diff --git a/Tests/StreamChatUITests/SnapshotTests/VoiceRecording/Components/BiDirectionalPanGestureRecogniser_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/VoiceRecording/Components/BiDirectionalPanGestureRecogniser_Tests.swift index 5afc388bc55..99971e588ae 100644 --- a/Tests/StreamChatUITests/SnapshotTests/VoiceRecording/Components/BiDirectionalPanGestureRecogniser_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/VoiceRecording/Components/BiDirectionalPanGestureRecogniser_Tests.swift @@ -3,8 +3,8 @@ // import StreamChat +import StreamChatTestTools import StreamChatUI -import StreamSwiftTestHelpers import XCTest final class BiDirectionalPanGestureRecogniser_Tests: XCTestCase {