diff --git a/.github/workflows/publish-to-trunk-workflow.yml b/.github/workflows/publish-to-trunk-workflow.yml deleted file mode 100644 index 0dcbbe0..0000000 --- a/.github/workflows/publish-to-trunk-workflow.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Publish to Trunk -on: - push: - tags: - - '*' -jobs: - build: - runs-on: macOS-latest - steps: - - uses: actions/checkout@v1 - - name: Install Cocoapods - run: gem install cocoapods - - name: Deploy to Cocoapods - run: | - set -eo pipefail - export LIB_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`) - pod lib lint --allow-warnings - pod trunk push --allow-warnings - env: - COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} diff --git a/.github/workflows/pull-request-workflow.yml b/.github/workflows/pull-request-workflow.yml index 30789a8..876d6cf 100644 --- a/.github/workflows/pull-request-workflow.yml +++ b/.github/workflows/pull-request-workflow.yml @@ -5,10 +5,6 @@ jobs: runs-on: macOS-latest timeout-minutes: 15 steps: - - name: Cancel previous jobs - uses: styfle/cancel-workflow-action@0.6.0 - with: - access_token: ${{ github.token }} - name: Git checkout uses: actions/checkout@v2.3.4 with: @@ -18,22 +14,9 @@ jobs: uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable - - name: Setup ruby and bundler dependencies - uses: ruby/setup-ruby@v1.81.0 - with: - bundler-cache: true - - name: Build for testing - run: | - xcodebuild build-for-testing \ - -scheme Shock \ - -destination "platform=iOS Simulator,OS=15.2,name=iPhone 13" - - name: Test without building - run: | - xcodebuild test-without-building \ - -scheme Shock \ - -destination "platform=iOS Simulator,OS=15.2,name=iPhone 13" - - name: Validate pod - run: | - set -eo pipefail - export LIB_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`) - bundle exec pod lib lint --allow-warnings + - name: Delete build artifacts + shell: bash + run: swift package clean + - name: Run tests + shell: bash + run: swift test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 084ee97..3abd61c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,18 @@ -# OS X -.DS_Store - # Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ +DerivedData/ +*.moved-aside *.pbxuser !default.pbxuser *.mode1v3 @@ -11,32 +21,82 @@ build/ !default.mode2v3 *.perspectivev3 !default.perspectivev3 -xcuserdata/ -*.xccheckout -profile -*.moved-aside -DerivedData + +## Obj-C/Swift specific *.hmap + +## App packaging *.ipa -.build +*.dSYM.zip +*.dSYM -# Bundler -.bundle +## Playgrounds +timeline.xctimeline +playground.xcworkspace -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm -Carthage/Build +.build/ +# CocoaPods +# # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control -# -# Note: if you ignore the Pods directory, make sure to uncomment -# `pod install` in .travis.yml -# -Pods/ -default.profraw -fastlane/test_output/ +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + fastlane/report.xml -derived_data/ +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +# macOS +.DS_Store + +# Git +*.orig + +# Object files +.build/ + +# Brew +Brewfile.lock.json diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index b502146..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -3.0.2 diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Shock.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Shock.xcscheme index 4352d4f..586e2f4 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Shock.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Shock.xcscheme @@ -1,6 +1,6 @@ + buildForRunning = "NO" + buildForProfiling = "NO" + buildForArchiving = "NO" + buildForAnalyzing = "NO"> @@ -60,9 +60,9 @@ skipped = "NO"> @@ -88,9 +88,9 @@ diff --git a/Demo/BuildConfigurations/DemoApp.xcconfig b/Demo/BuildConfigurations/DemoApp.xcconfig new file mode 100644 index 0000000..a92ffd2 --- /dev/null +++ b/Demo/BuildConfigurations/DemoApp.xcconfig @@ -0,0 +1,21 @@ +ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon +CODE_SIGN_IDENTITY=Apple Development +CODE_SIGN_STYLE=Automatic +DEVELOPMENT_TEAM= +ENABLE_NS_ASSERTIONS=NO +ENABLE_PREVIEWS=YES +ENABLE_TESTABILITY=YES +FRAMEWORK_SEARCH_PATHS=$(inherited) +GCC_OPTIMIZATION_LEVEL[config=Debug]=0 +GCC_OPTIMIZATION_LEVEL[config=Release]=fast +GCC_PREPROCESSOR_DEFINITIONS[config=Debug]=DEBUG=1 $(inherited) +LD_RUNPATH_SEARCH_PATHS=$(inherited) @executable_path/Frameworks +MTL_ENABLE_DEBUG_INFO[config=Debug]=YES +MTL_ENABLE_DEBUG_INFO[config=Release]=NO +PRODUCT_BUNDLE_IDENTIFIER=com.justeattakeaway.shock.demoapp +PRODUCT_MODULE_NAME=$(TARGET_NAME:c99extidentifier) +PRODUCT_NAME=$(TARGET_NAME) +PROVISIONING_PROFILE_SPECIFIER[config=Debug]= +PROVISIONING_PROFILE_SPECIFIER[config=Release]= +SWIFT_OPTIMIZATION_LEVEL=-Onone +TARGETED_DEVICE_FAMILY=1,2 diff --git a/Demo/BuildConfigurations/Project.xcconfig b/Demo/BuildConfigurations/Project.xcconfig new file mode 100644 index 0000000..764f405 --- /dev/null +++ b/Demo/BuildConfigurations/Project.xcconfig @@ -0,0 +1,55 @@ +ALWAYS_SEARCH_USER_PATHS=NO +CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED=YES +CLANG_CXX_LANGUAGE_STANDARD=gnu++0x +CLANG_CXX_LIBRARY=libc++ +CLANG_ENABLE_MODULES=YES +CLANG_ENABLE_OBJC_ARC=YES +CLANG_WARN__DUPLICATE_METHOD_MATCH=YES +CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING=YES +CLANG_WARN_BOOL_CONVERSION=YES +CLANG_WARN_COMMA=YES +CLANG_WARN_CONSTANT_CONVERSION=YES +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS=YES +CLANG_WARN_DIRECT_OBJC_ISA_USAGE=YES_ERROR +CLANG_WARN_EMPTY_BODY=YES +CLANG_WARN_ENUM_CONVERSION=YES +CLANG_WARN_INFINITE_RECURSION=YES +CLANG_WARN_INT_CONVERSION=YES +CLANG_WARN_NON_LITERAL_NULL_CONVERSION=YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF=YES +CLANG_WARN_OBJC_LITERAL_CONVERSION=YES +CLANG_WARN_OBJC_ROOT_CLASS=YES_ERROR +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER=YES +CLANG_WARN_RANGE_LOOP_ANALYSIS=YES +CLANG_WARN_STRICT_PROTOTYPES=YES +CLANG_WARN_SUSPICIOUS_MOVE=YES +CLANG_WARN_UNREACHABLE_CODE=YES +COPY_PHASE_STRIP=NO +CURRENT_PROJECT_VERSION=1 +DEBUG_INFORMATION_FORMAT[config=Debug]=dwarf +DEBUG_INFORMATION_FORMAT[config=Release]=dwarf-with-dsym +ENABLE_NS_ASSERTIONS[config=Debug]=YES +ENABLE_NS_ASSERTIONS[config=Release]=NO +ENABLE_STRICT_OBJC_MSGSEND=YES +GCC_C_LANGUAGE_STANDARD=gnu99 +GCC_DYNAMIC_NO_PIC=NO +GCC_NO_COMMON_BLOCKS=YES +GCC_OPTIMIZATION_LEVEL=fast +GCC_SYMBOLS_PRIVATE_EXTERN=NO +GCC_TREAT_WARNINGS_AS_ERRORS=YES +GCC_WARN_64_TO_32_BIT_CONVERSION=YES +GCC_WARN_ABOUT_RETURN_TYPE=YES_ERROR +GCC_WARN_UNDECLARED_SELECTOR=YES +GCC_WARN_UNINITIALIZED_AUTOS=YES_AGGRESSIVE +GCC_WARN_UNUSED_FUNCTION=YES +GCC_WARN_UNUSED_VARIABLE=YES +IPHONEOS_DEPLOYMENT_TARGET=15.2 +MARKETING_VERSION=1.0 +ONLY_ACTIVE_ARCH=YES +OTHER_LDFLAGS=$(OTHER_LDFLAGS) -ObjC +SDKROOT=iphoneos +SWIFT_OPTIMIZATION_LEVEL=-O +SWIFT_TREAT_WARNINGS_AS_ERRORS=YES +SWIFT_VERSION=5.0 +VALIDATE_PRODUCT=YES +VERSIONING_SYSTEM=apple-generic diff --git a/Demo/BuildConfigurations/UITests.xcconfig b/Demo/BuildConfigurations/UITests.xcconfig new file mode 100644 index 0000000..81c0e3a --- /dev/null +++ b/Demo/BuildConfigurations/UITests.xcconfig @@ -0,0 +1,9 @@ +CLANG_ANALYZER_NONNULL=YES +CLANG_WARN_DOCUMENTATION_COMMENTS=YES +INFOPLIST_FILE=UITests/Info.plist +LD_RUNPATH_SEARCH_PATHS=$(inherited) @executable_path/Frameworks @loader_path/Frameworks +PRODUCT_BUNDLE_IDENTIFIER=com.justeattakeaway.shock.uitests +PRODUCT_NAME=$(TARGET_NAME) +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug]=DEBUG +SWIFT_OPTIMIZATION_LEVEL=-O +TEST_TARGET_NAME=DemoApp diff --git a/Demo/DemoApp.xcodeproj/project.pbxproj b/Demo/DemoApp.xcodeproj/project.pbxproj index 9592634..1cd17bc 100644 --- a/Demo/DemoApp.xcodeproj/project.pbxproj +++ b/Demo/DemoApp.xcodeproj/project.pbxproj @@ -7,108 +7,245 @@ objects = { /* Begin PBXBuildFile section */ - 4F90FAF42868B2530073D6E5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F90FAF32868B2530073D6E5 /* AppDelegate.swift */; }; - 4F90FAF62868B2530073D6E5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F90FAF52868B2530073D6E5 /* SceneDelegate.swift */; }; - 4F90FAF82868B2530073D6E5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F90FAF72868B2530073D6E5 /* ViewController.swift */; }; - 4F90FB0A2868B2DC0073D6E5 /* MyRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F90FB092868B2DC0073D6E5 /* MyRoutes.swift */; }; - 4F90FB182868B5EA0073D6E5 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4F90FB0F2868B5EA0073D6E5 /* LaunchScreen.xib */; }; - 4F90FB192868B5EA0073D6E5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4F90FB112868B5EA0073D6E5 /* Main.storyboard */; }; - 4F90FB1A2868B5EA0073D6E5 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F90FB132868B5EA0073D6E5 /* Images.xcassets */; }; - 4F90FB1B2868B5EA0073D6E5 /* template-route.json.mustache in Resources */ = {isa = PBXBuildFile; fileRef = 4F90FB152868B5EA0073D6E5 /* template-route.json.mustache */; }; - 4F90FB1C2868B5EA0073D6E5 /* custom-route.json in Resources */ = {isa = PBXBuildFile; fileRef = 4F90FB162868B5EA0073D6E5 /* custom-route.json */; }; - 4F90FB1D2868B5EA0073D6E5 /* simple-route.json in Resources */ = {isa = PBXBuildFile; fileRef = 4F90FB172868B5EA0073D6E5 /* simple-route.json */; }; - 4FB1CF5C2868C0D30035E5FF /* Shock in Frameworks */ = {isa = PBXBuildFile; productRef = 4FB1CF5B2868C0D30035E5FF /* Shock */; }; + 04A7A369D4FB3FC20DD8B665 /* 320d4092848f48cbfd0df5fd94b4af22.json in Resources */ = {isa = PBXBuildFile; fileRef = 57C4D036E520F69C5BEAAF77 /* 320d4092848f48cbfd0df5fd94b4af22.json */; }; + 0766A839F872DE9357438A95 /* ShockRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3708E4D8D3089DFA60F2D69 /* ShockRecorder.swift */; }; + 0D51BA99D4C6370D28DFC929 /* template-route.json.mustache in Resources */ = {isa = PBXBuildFile; fileRef = 66A53013B2EDE695CCC4CF7C /* template-route.json.mustache */; }; + 0F53A0E93A9D2DCF0E986D2E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55FA0DEBEC2C2A35112BF30B /* ViewController.swift */; }; + 1B11E3EC5B9F0B5ABD5481D1 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = B3BE30F9012614836AC0501E /* LaunchScreen.xib */; }; + 284BBAE16B20FD214445FC88 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D42A3D554F4B272C94220B /* AppDelegate.swift */; }; + 2D4C4FBE338A4C0DEC428D89 /* UITestOutputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BE17253A54E04B19101B0D /* UITestOutputConfig.swift */; }; + 4C2E4990A301C60B1E21B0A1 /* MyRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC825E5B23C35EA1610AC32 /* MyRoutes.swift */; }; + 64B7D448F260F1C8500F9DBA /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 888A36EB491A82652E51B04A /* Images.xcassets */; }; + 6856B6C7FB80BD110887EB15 /* 2023-09-15-14-46-21_001_GET_api_breeds_image_random.json in Resources */ = {isa = PBXBuildFile; fileRef = AABE12130842B45E0E077EF5 /* 2023-09-15-14-46-21_001_GET_api_breeds_image_random.json */; }; + 6C950959E4A54A23C3FEF6A7 /* RecorderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E611FA6317F23F9E923977DE /* RecorderViewController.swift */; }; + 729AC1EDA3A7A11A3BE74966 /* simple-route.json in Resources */ = {isa = PBXBuildFile; fileRef = E2DEA7C7F00A58E1101E3A2A /* simple-route.json */; }; + 795660945878BC1AFFDDF2F8 /* DemoAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC18B8B7BD2B512DDB878316 /* DemoAppUITests.swift */; }; + 7D5CA18E98D16C841737A367 /* MockRoutesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A47091BE47BBA74BB4AFBB9 /* MockRoutesManager.swift */; }; + 7E3255EDD2C9356017AA385B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FE064E47CF636DFA0C8635EA /* Main.storyboard */; }; + 92EA1A6AC797F29327270419 /* UITestOutputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F65628043445C5E1342E594 /* UITestOutputConfig.swift */; }; + 94764F8CF09658D4219A0B82 /* Shock in Frameworks */ = {isa = PBXBuildFile; productRef = 8B5DFE4E3B9C70FFC5CD770B /* Shock */; }; + C2D254935EBAC651BF4BD714 /* DogAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7422209979C604B3E61C1D /* DogAPI.swift */; }; + D010E4B57F86DA1209187B47 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDF8E4691C4A6E4875C0E45 /* SceneDelegate.swift */; }; + DFA0256A139D0EF6D51130AF /* Shock in Frameworks */ = {isa = PBXBuildFile; productRef = 233B5785EC902A95C6E1D08A /* Shock */; }; + EC806109D1BDF1F99A9E565F /* custom-route.json in Resources */ = {isa = PBXBuildFile; fileRef = C8A0AE0C2A9C4BBBCA9C0AD7 /* custom-route.json */; }; /* End PBXBuildFile section */ +/* Begin PBXCopyFilesBuildPhase section */ + 4881520E4043C49D07923ABB /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + C01B5294BE8ED179CA059E63 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ - 4F90FAF02868B2530073D6E5 /* DemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 4F90FAF32868B2530073D6E5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 4F90FAF52868B2530073D6E5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 4F90FAF72868B2530073D6E5 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 4F90FB012868B2540073D6E5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 4F90FB082868B2BF0073D6E5 /* Shock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Shock; path = ..; sourceTree = ""; }; - 4F90FB092868B2DC0073D6E5 /* MyRoutes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyRoutes.swift; sourceTree = ""; }; - 4F90FB102868B5EA0073D6E5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; - 4F90FB122868B5EA0073D6E5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 4F90FB132868B5EA0073D6E5 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - 4F90FB152868B5EA0073D6E5 /* template-route.json.mustache */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "template-route.json.mustache"; sourceTree = ""; }; - 4F90FB162868B5EA0073D6E5 /* custom-route.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "custom-route.json"; sourceTree = ""; }; - 4F90FB172868B5EA0073D6E5 /* simple-route.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "simple-route.json"; sourceTree = ""; }; + 06F917EC0C8A5B6F2ED3B5EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 0F65628043445C5E1342E594 /* UITestOutputConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestOutputConfig.swift; sourceTree = ""; }; + 1A47091BE47BBA74BB4AFBB9 /* MockRoutesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoutesManager.swift; sourceTree = ""; }; + 3A0DF2237FEA2891893C16CF /* Shock-GitHub */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "Shock-GitHub"; path = ..; sourceTree = ""; }; + 55FA0DEBEC2C2A35112BF30B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 57C4D036E520F69C5BEAAF77 /* 320d4092848f48cbfd0df5fd94b4af22.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = 320d4092848f48cbfd0df5fd94b4af22.json; sourceTree = ""; }; + 619A93EE9E77E6656461FA80 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + 64ADAEE20B5238835DBFED6F /* Project.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Project.xcconfig; sourceTree = ""; }; + 66A53013B2EDE695CCC4CF7C /* template-route.json.mustache */ = {isa = PBXFileReference; lastKnownFileType = text; path = "template-route.json.mustache"; sourceTree = ""; }; + 888A36EB491A82652E51B04A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 8D59A62EA82E9161E5F06861 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 8DDF8E4691C4A6E4875C0E45 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + A3708E4D8D3089DFA60F2D69 /* ShockRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShockRecorder.swift; sourceTree = ""; }; + A5CCBF9ABADD2C9E9D0A698E /* UITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UITests.xcconfig; sourceTree = ""; }; + A8BE17253A54E04B19101B0D /* UITestOutputConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestOutputConfig.swift; sourceTree = ""; }; + AABE12130842B45E0E077EF5 /* 2023-09-15-14-46-21_001_GET_api_breeds_image_random.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "2023-09-15-14-46-21_001_GET_api_breeds_image_random.json"; sourceTree = ""; }; + B537789CBC16B46423224858 /* DemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C7F3005E46140059E5AF9DC8 /* UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + C8A0AE0C2A9C4BBBCA9C0AD7 /* custom-route.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "custom-route.json"; sourceTree = ""; }; + CDEDD415A3DE6E2843E69851 /* DemoApp.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DemoApp.xcconfig; sourceTree = ""; }; + D7D42A3D554F4B272C94220B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + DC18B8B7BD2B512DDB878316 /* DemoAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoAppUITests.swift; sourceTree = ""; }; + E2DEA7C7F00A58E1101E3A2A /* simple-route.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "simple-route.json"; sourceTree = ""; }; + E611FA6317F23F9E923977DE /* RecorderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecorderViewController.swift; sourceTree = ""; }; + EA7422209979C604B3E61C1D /* DogAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogAPI.swift; sourceTree = ""; }; + EDC825E5B23C35EA1610AC32 /* MyRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyRoutes.swift; sourceTree = ""; }; + FC3EA90D1D98063D287350DA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 4F90FAED2868B2530073D6E5 /* Frameworks */ = { + 631EEB8FD7848E1438134A16 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4FB1CF5C2868C0D30035E5FF /* Shock in Frameworks */, + DFA0256A139D0EF6D51130AF /* Shock in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6F40A38F900FA47D1182229C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 94764F8CF09658D4219A0B82 /* Shock in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 4F90FAE72868B2530073D6E5 = { + 23A977908DE2262EF5525631 /* Data */ = { isa = PBXGroup; children = ( - 4F90FB072868B2BF0073D6E5 /* Packages */, - 4F90FB0E2868B5EA0073D6E5 /* Resources */, - 4F90FAF22868B2530073D6E5 /* Sources */, - 4F90FAF12868B2530073D6E5 /* Products */, - 4FB1CF5A2868C0D30035E5FF /* Frameworks */, + C8A0AE0C2A9C4BBBCA9C0AD7 /* custom-route.json */, + E2DEA7C7F00A58E1101E3A2A /* simple-route.json */, + 66A53013B2EDE695CCC4CF7C /* template-route.json.mustache */, ); + path = Data; sourceTree = ""; }; - 4F90FAF12868B2530073D6E5 /* Products */ = { + 440193E401A4E83EEA00B144 = { isa = PBXGroup; children = ( - 4F90FAF02868B2530073D6E5 /* DemoApp.app */, + 772308CE664846AE1ABFECB2 /* Project */, + E4DDE77CF2D86911C0CB81BE /* Frameworks */, + D4CC74EBFCAF08A1E61D64BF /* Products */, + 3A0DF2237FEA2891893C16CF /* Shock-GitHub */, ); - name = Products; + indentWidth = 4; sourceTree = ""; + tabWidth = 4; + usesTabs = 0; + wrapsLines = 1; }; - 4F90FAF22868B2530073D6E5 /* Sources */ = { + 52126BA9BE3531725C29CBE6 /* Sources */ = { isa = PBXGroup; children = ( - 4F90FAF32868B2530073D6E5 /* AppDelegate.swift */, - 4F90FAF52868B2530073D6E5 /* SceneDelegate.swift */, - 4F90FAF72868B2530073D6E5 /* ViewController.swift */, - 4F90FB092868B2DC0073D6E5 /* MyRoutes.swift */, - 4F90FB012868B2540073D6E5 /* Info.plist */, + D7D42A3D554F4B272C94220B /* AppDelegate.swift */, + EA7422209979C604B3E61C1D /* DogAPI.swift */, + 1A47091BE47BBA74BB4AFBB9 /* MockRoutesManager.swift */, + EDC825E5B23C35EA1610AC32 /* MyRoutes.swift */, + E611FA6317F23F9E923977DE /* RecorderViewController.swift */, + 8DDF8E4691C4A6E4875C0E45 /* SceneDelegate.swift */, + A3708E4D8D3089DFA60F2D69 /* ShockRecorder.swift */, + A8BE17253A54E04B19101B0D /* UITestOutputConfig.swift */, + 55FA0DEBEC2C2A35112BF30B /* ViewController.swift */, ); path = Sources; sourceTree = ""; }; - 4F90FB072868B2BF0073D6E5 /* Packages */ = { + 59DE962A4286194F215F15DA /* BuildConfigurations */ = { + isa = PBXGroup; + children = ( + CDEDD415A3DE6E2843E69851 /* DemoApp.xcconfig */, + 64ADAEE20B5238835DBFED6F /* Project.xcconfig */, + A5CCBF9ABADD2C9E9D0A698E /* UITests.xcconfig */, + ); + path = BuildConfigurations; + sourceTree = ""; + }; + 772308CE664846AE1ABFECB2 /* Project */ = { isa = PBXGroup; children = ( - 4F90FB082868B2BF0073D6E5 /* Shock */, + 59DE962A4286194F215F15DA /* BuildConfigurations */, + 955FC132E180DA57EE328C06 /* DemoApp */, + B4357A58A4473790D90CFEB5 /* UITests */, ); - name = Packages; + name = Project; sourceTree = ""; }; - 4F90FB0E2868B5EA0073D6E5 /* Resources */ = { + 785CB4FF52614F11AAC2C859 /* Resources */ = { isa = PBXGroup; children = ( - 4F90FB0F2868B5EA0073D6E5 /* LaunchScreen.xib */, - 4F90FB112868B5EA0073D6E5 /* Main.storyboard */, - 4F90FB132868B5EA0073D6E5 /* Images.xcassets */, - 4F90FB142868B5EA0073D6E5 /* Data */, + 23A977908DE2262EF5525631 /* Data */, + B3BE30F9012614836AC0501E /* LaunchScreen.xib */, + FE064E47CF636DFA0C8635EA /* Main.storyboard */, + 888A36EB491A82652E51B04A /* Images.xcassets */, ); path = Resources; sourceTree = ""; }; - 4F90FB142868B5EA0073D6E5 /* Data */ = { + 7A15AB8A13A36D8FA4AC98CA /* Resources */ = { isa = PBXGroup; children = ( - 4F90FB152868B5EA0073D6E5 /* template-route.json.mustache */, - 4F90FB162868B5EA0073D6E5 /* custom-route.json */, - 4F90FB172868B5EA0073D6E5 /* simple-route.json */, + A1B89AB6DDA8DB33A2E49822 /* RecordedMocks */, ); - path = Data; + path = Resources; sourceTree = ""; }; - 4FB1CF5A2868C0D30035E5FF /* Frameworks */ = { + 955FC132E180DA57EE328C06 /* DemoApp */ = { + isa = PBXGroup; + children = ( + 785CB4FF52614F11AAC2C859 /* Resources */, + 52126BA9BE3531725C29CBE6 /* Sources */, + 8D59A62EA82E9161E5F06861 /* Info.plist */, + ); + path = DemoApp; + sourceTree = ""; + }; + 98D39E235539108BBA012127 /* Sources */ = { + isa = PBXGroup; + children = ( + DC18B8B7BD2B512DDB878316 /* DemoAppUITests.swift */, + 0F65628043445C5E1342E594 /* UITestOutputConfig.swift */, + ); + path = Sources; + sourceTree = ""; + }; + A1B89AB6DDA8DB33A2E49822 /* RecordedMocks */ = { + isa = PBXGroup; + children = ( + D9FEEDF7B24CCD97DDF59103 /* DemoAppUITests */, + ); + path = RecordedMocks; + sourceTree = ""; + }; + ADE58ECB0104E10B246412AA /* 320d4092848f48cbfd0df5fd94b4af22 */ = { + isa = PBXGroup; + children = ( + AABE12130842B45E0E077EF5 /* 2023-09-15-14-46-21_001_GET_api_breeds_image_random.json */, + ); + path = 320d4092848f48cbfd0df5fd94b4af22; + sourceTree = ""; + }; + B4357A58A4473790D90CFEB5 /* UITests */ = { + isa = PBXGroup; + children = ( + 7A15AB8A13A36D8FA4AC98CA /* Resources */, + 98D39E235539108BBA012127 /* Sources */, + FC3EA90D1D98063D287350DA /* Info.plist */, + ); + path = UITests; + sourceTree = ""; + }; + D4CC74EBFCAF08A1E61D64BF /* Products */ = { + isa = PBXGroup; + children = ( + B537789CBC16B46423224858 /* DemoApp.app */, + C7F3005E46140059E5AF9DC8 /* UITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + D9FEEDF7B24CCD97DDF59103 /* DemoAppUITests */ = { + isa = PBXGroup; + children = ( + ADE58ECB0104E10B246412AA /* 320d4092848f48cbfd0df5fd94b4af22 */, + 57C4D036E520F69C5BEAAF77 /* 320d4092848f48cbfd0df5fd94b4af22.json */, + ); + path = DemoAppUITests; + sourceTree = ""; + }; + E4DDE77CF2D86911C0CB81BE /* Frameworks */ = { isa = PBXGroup; children = ( ); @@ -118,110 +255,141 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 4F90FAEF2868B2530073D6E5 /* DemoApp */ = { + 8F97EC1C6B99739E5669DEEE /* UITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E3B645DE3103BDB9FAC9C202 /* Build configuration list for PBXNativeTarget "UITests" */; + buildPhases = ( + BFBB574C656BC57443871235 /* Sources */, + DFB0EF44ED37B3EBC5453AA2 /* Resources */, + 4881520E4043C49D07923ABB /* Embed Frameworks */, + 6F40A38F900FA47D1182229C /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = UITests; + packageProductDependencies = ( + 8B5DFE4E3B9C70FFC5CD770B /* Shock */, + ); + productName = UITests; + productReference = C7F3005E46140059E5AF9DC8 /* UITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + CD9365CEE296087B81D5EDE0 /* DemoApp */ = { isa = PBXNativeTarget; - buildConfigurationList = 4F90FB042868B2540073D6E5 /* Build configuration list for PBXNativeTarget "DemoApp" */; + buildConfigurationList = 6BF92CDD7C5A429EE9438DB2 /* Build configuration list for PBXNativeTarget "DemoApp" */; buildPhases = ( - 4F90FAEC2868B2530073D6E5 /* Sources */, - 4F90FAED2868B2530073D6E5 /* Frameworks */, - 4F90FAEE2868B2530073D6E5 /* Resources */, + 06C44C80621A0C39B6AB7A54 /* Sources */, + F3621958ACE022B3E174CC1E /* Resources */, + C01B5294BE8ED179CA059E63 /* Embed Frameworks */, + 631EEB8FD7848E1438134A16 /* Frameworks */, ); buildRules = ( ); dependencies = ( - 4F90FB0D2868B4180073D6E5 /* PBXTargetDependency */, ); name = DemoApp; packageProductDependencies = ( - 4FB1CF5B2868C0D30035E5FF /* Shock */, + 233B5785EC902A95C6E1D08A /* Shock */, ); productName = DemoApp; - productReference = 4F90FAF02868B2530073D6E5 /* DemoApp.app */; + productReference = B537789CBC16B46423224858 /* DemoApp.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 4F90FAE82868B2530073D6E5 /* Project object */ = { + 3ACCC0531C14DF86AB1980F5 /* Project object */ = { isa = PBXProject; attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1340; - LastUpgradeCheck = 1340; - TargetAttributes = { - 4F90FAEF2868B2530073D6E5 = { - CreatedOnToolsVersion = 13.4.1; - }; - }; + ORGANIZATIONNAME = "Just Eat Takeaway"; }; - buildConfigurationList = 4F90FAEB2868B2530073D6E5 /* Build configuration list for PBXProject "DemoApp" */; + buildConfigurationList = B9C4410D5AD85BE6945FC336 /* Build configuration list for PBXProject "DemoApp" */; compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( - en, Base, + en, ); - mainGroup = 4F90FAE72868B2530073D6E5; - productRefGroup = 4F90FAF12868B2530073D6E5 /* Products */; + mainGroup = 440193E401A4E83EEA00B144; + productRefGroup = D4CC74EBFCAF08A1E61D64BF /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 4F90FAEF2868B2530073D6E5 /* DemoApp */, + CD9365CEE296087B81D5EDE0 /* DemoApp */, + 8F97EC1C6B99739E5669DEEE /* UITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 4F90FAEE2868B2530073D6E5 /* Resources */ = { + DFB0EF44ED37B3EBC5453AA2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 04A7A369D4FB3FC20DD8B665 /* 320d4092848f48cbfd0df5fd94b4af22.json in Resources */, + 6856B6C7FB80BD110887EB15 /* 2023-09-15-14-46-21_001_GET_api_breeds_image_random.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F3621958ACE022B3E174CC1E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4F90FB1C2868B5EA0073D6E5 /* custom-route.json in Resources */, - 4F90FB1B2868B5EA0073D6E5 /* template-route.json.mustache in Resources */, - 4F90FB1A2868B5EA0073D6E5 /* Images.xcassets in Resources */, - 4F90FB182868B5EA0073D6E5 /* LaunchScreen.xib in Resources */, - 4F90FB192868B5EA0073D6E5 /* Main.storyboard in Resources */, - 4F90FB1D2868B5EA0073D6E5 /* simple-route.json in Resources */, + 1B11E3EC5B9F0B5ABD5481D1 /* LaunchScreen.xib in Resources */, + 7E3255EDD2C9356017AA385B /* Main.storyboard in Resources */, + EC806109D1BDF1F99A9E565F /* custom-route.json in Resources */, + 729AC1EDA3A7A11A3BE74966 /* simple-route.json in Resources */, + 0D51BA99D4C6370D28DFC929 /* template-route.json.mustache in Resources */, + 64B7D448F260F1C8500F9DBA /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 4F90FAEC2868B2530073D6E5 /* Sources */ = { + 06C44C80621A0C39B6AB7A54 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4F90FAF82868B2530073D6E5 /* ViewController.swift in Sources */, - 4F90FB0A2868B2DC0073D6E5 /* MyRoutes.swift in Sources */, - 4F90FAF42868B2530073D6E5 /* AppDelegate.swift in Sources */, - 4F90FAF62868B2530073D6E5 /* SceneDelegate.swift in Sources */, + 284BBAE16B20FD214445FC88 /* AppDelegate.swift in Sources */, + C2D254935EBAC651BF4BD714 /* DogAPI.swift in Sources */, + 7D5CA18E98D16C841737A367 /* MockRoutesManager.swift in Sources */, + 4C2E4990A301C60B1E21B0A1 /* MyRoutes.swift in Sources */, + 6C950959E4A54A23C3FEF6A7 /* RecorderViewController.swift in Sources */, + D010E4B57F86DA1209187B47 /* SceneDelegate.swift in Sources */, + 0766A839F872DE9357438A95 /* ShockRecorder.swift in Sources */, + 2D4C4FBE338A4C0DEC428D89 /* UITestOutputConfig.swift in Sources */, + 0F53A0E93A9D2DCF0E986D2E /* ViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 4F90FB0D2868B4180073D6E5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = 4F90FB0C2868B4180073D6E5 /* Shock */; + BFBB574C656BC57443871235 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 795660945878BC1AFFDDF2F8 /* DemoAppUITests.swift in Sources */, + 92EA1A6AC797F29327270419 /* UITestOutputConfig.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; }; -/* End PBXTargetDependency section */ +/* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - 4F90FB0F2868B5EA0073D6E5 /* LaunchScreen.xib */ = { + B3BE30F9012614836AC0501E /* LaunchScreen.xib */ = { isa = PBXVariantGroup; children = ( - 4F90FB102868B5EA0073D6E5 /* Base */, + 619A93EE9E77E6656461FA80 /* Base */, ); name = LaunchScreen.xib; sourceTree = ""; }; - 4F90FB112868B5EA0073D6E5 /* Main.storyboard */ = { + FE064E47CF636DFA0C8635EA /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( - 4F90FB122868B5EA0073D6E5 /* Base */, + 06F917EC0C8A5B6F2ED3B5EA /* Base */, ); name = Main.storyboard; sourceTree = ""; @@ -229,193 +397,106 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 4F90FB022868B2540073D6E5 /* Debug */ = { + 041C67ECF87CA2F42B2A6BA2 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 64ADAEE20B5238835DBFED6F /* Project.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 17707D7259CA9368A4B03200 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 64ADAEE20B5238835DBFED6F /* Project.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 1C545C0DD7857285CD8FC0E0 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = CDEDD415A3DE6E2843E69851 /* DemoApp.xcconfig */; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; + INFOPLIST_FILE = DemoApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = DemoApp; SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - 4F90FB032868B2540073D6E5 /* Release */ = { + 48032F223944D50CDC13C50C /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A5CCBF9ABADD2C9E9D0A698E /* UITests.xcconfig */; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; + INFOPLIST_FILE = UITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = UITests; SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; - 4F90FB052868B2540073D6E5 /* Debug */ = { + AD50BDF0C399EC95218708BF /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = CDEDD415A3DE6E2843E69851 /* DemoApp.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ZKPN288GRK; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = Sources/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.justeattakeaway.DemoApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + INFOPLIST_FILE = DemoApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = DemoApp; + SDKROOT = iphoneos; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; - name = Debug; + name = Release; }; - 4F90FB062868B2540073D6E5 /* Release */ = { + AEEE980215D89F19D586C778 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A5CCBF9ABADD2C9E9D0A698E /* UITests.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ZKPN288GRK; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = Sources/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.justeattakeaway.DemoApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + INFOPLIST_FILE = UITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = UITests; + SDKROOT = iphoneos; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; - name = Release; + name = Debug; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 4F90FAEB2868B2530073D6E5 /* Build configuration list for PBXProject "DemoApp" */ = { + 6BF92CDD7C5A429EE9438DB2 /* Build configuration list for PBXNativeTarget "DemoApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1C545C0DD7857285CD8FC0E0 /* Debug */, + AD50BDF0C399EC95218708BF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B9C4410D5AD85BE6945FC336 /* Build configuration list for PBXProject "DemoApp" */ = { isa = XCConfigurationList; buildConfigurations = ( - 4F90FB022868B2540073D6E5 /* Debug */, - 4F90FB032868B2540073D6E5 /* Release */, + 17707D7259CA9368A4B03200 /* Debug */, + 041C67ECF87CA2F42B2A6BA2 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 4F90FB042868B2540073D6E5 /* Build configuration list for PBXNativeTarget "DemoApp" */ = { + E3B645DE3103BDB9FAC9C202 /* Build configuration list for PBXNativeTarget "UITests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 4F90FB052868B2540073D6E5 /* Debug */, - 4F90FB062868B2540073D6E5 /* Release */, + AEEE980215D89F19D586C778 /* Debug */, + 48032F223944D50CDC13C50C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -423,15 +504,15 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 4F90FB0C2868B4180073D6E5 /* Shock */ = { + 233B5785EC902A95C6E1D08A /* Shock */ = { isa = XCSwiftPackageProductDependency; productName = Shock; }; - 4FB1CF5B2868C0D30035E5FF /* Shock */ = { + 8B5DFE4E3B9C70FFC5CD770B /* Shock */ = { isa = XCSwiftPackageProductDependency; productName = Shock; }; /* End XCSwiftPackageProductDependency section */ }; - rootObject = 4F90FAE82868B2530073D6E5 /* Project object */; + rootObject = 3ACCC0531C14DF86AB1980F5 /* Project object */; } diff --git a/Demo/DemoApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Demo/DemoApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 77c632b..52b8c61 100644 --- a/Demo/DemoApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Demo/DemoApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,35 +5,35 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRMustache.swift", "state" : { - "revision" : "671e2c5234e829c1f337c9d300bd6d9b21d1c42a", - "version" : "4.0.1" + "revision" : "edbe65da33671ca1e93e0751cbbeffc893b48da8", + "version" : "4.1.0" } }, { - "identity" : "justlog", + "identity" : "swift-atomics", "kind" : "remoteSourceControl", - "location" : "https://github.com/justeat/JustLog", + "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "b9bb34d2ae6d3d5e7d2ef678f588718ed96d643e", - "version" : "4.0.2" + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" } }, { - "identity" : "swift-nio", + "identity" : "swift-collections", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio", + "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "124119f0bb12384cef35aa041d7c3a686108722d", - "version" : "2.40.0" + "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", + "version" : "1.0.5" } }, { - "identity" : "swiftybeaver", + "identity" : "swift-nio", "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftyBeaver/SwiftyBeaver", + "location" : "https://github.com/apple/swift-nio", "state" : { - "revision" : "12b5acf96d98f91d50de447369bd18df74600f1a", - "version" : "1.9.6" + "revision" : "f7c46552983b06b0958a1a4c8bc5199406ae4c8a", + "version" : "2.51.0" } } ], diff --git a/Demo/DemoApp.xcodeproj/xcshareddata/xcschemes/DemoApp.xcscheme b/Demo/DemoApp.xcodeproj/xcshareddata/xcschemes/DemoApp.xcscheme new file mode 100644 index 0000000..9f04123 --- /dev/null +++ b/Demo/DemoApp.xcodeproj/xcshareddata/xcschemes/DemoApp.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/DemoApp.xcodeproj/xcshareddata/xcschemes/UITests.xcscheme b/Demo/DemoApp.xcodeproj/xcshareddata/xcschemes/UITests.xcscheme new file mode 100644 index 0000000..4229155 --- /dev/null +++ b/Demo/DemoApp.xcodeproj/xcshareddata/xcschemes/UITests.xcscheme @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/DemoApp.xcworkspace/contents.xcworkspacedata b/Demo/DemoApp.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..9b17505 --- /dev/null +++ b/Demo/DemoApp.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Demo/DemoApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Demo/DemoApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Demo/DemoApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Demo/DemoApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Demo/DemoApp.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Demo/DemoApp.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..08de0be --- /dev/null +++ b/Demo/DemoApp.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + diff --git a/Demo/DemoApp.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Demo/DemoApp.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..722a2c1 --- /dev/null +++ b/Demo/DemoApp.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,41 @@ +{ + "pins" : [ + { + "identity" : "grmustache.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRMustache.swift", + "state" : { + "revision" : "edbe65da33671ca1e93e0751cbbeffc893b48da8", + "version" : "4.1.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio", + "state" : { + "revision" : "f7c46552983b06b0958a1a4c8bc5199406ae4c8a", + "version" : "2.51.0" + } + } + ], + "version" : 2 +} diff --git a/Demo/Info.plist b/Demo/DemoApp/Info.plist similarity index 100% rename from Demo/Info.plist rename to Demo/DemoApp/Info.plist diff --git a/Demo/Resources/Base.lproj/LaunchScreen.xib b/Demo/DemoApp/Resources/Base.lproj/LaunchScreen.xib similarity index 100% rename from Demo/Resources/Base.lproj/LaunchScreen.xib rename to Demo/DemoApp/Resources/Base.lproj/LaunchScreen.xib diff --git a/Demo/DemoApp/Resources/Base.lproj/Main.storyboard b/Demo/DemoApp/Resources/Base.lproj/Main.storyboard new file mode 100644 index 0000000..bbb3617 --- /dev/null +++ b/Demo/DemoApp/Resources/Base.lproj/Main.storyboard @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Resources/Data/custom-route.json b/Demo/DemoApp/Resources/Data/custom-route.json similarity index 100% rename from Demo/Resources/Data/custom-route.json rename to Demo/DemoApp/Resources/Data/custom-route.json diff --git a/Demo/Resources/Data/simple-route.json b/Demo/DemoApp/Resources/Data/simple-route.json similarity index 100% rename from Demo/Resources/Data/simple-route.json rename to Demo/DemoApp/Resources/Data/simple-route.json diff --git a/Demo/Resources/Data/template-route.json.mustache b/Demo/DemoApp/Resources/Data/template-route.json.mustache similarity index 100% rename from Demo/Resources/Data/template-route.json.mustache rename to Demo/DemoApp/Resources/Data/template-route.json.mustache diff --git a/Demo/Resources/Images.xcassets/AccentColor.colorset/Contents.json b/Demo/DemoApp/Resources/Images.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Demo/Resources/Images.xcassets/AccentColor.colorset/Contents.json rename to Demo/DemoApp/Resources/Images.xcassets/AccentColor.colorset/Contents.json diff --git a/Demo/Resources/Images.xcassets/AppIcon.appiconset/Contents.json b/Demo/DemoApp/Resources/Images.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Demo/Resources/Images.xcassets/AppIcon.appiconset/Contents.json rename to Demo/DemoApp/Resources/Images.xcassets/AppIcon.appiconset/Contents.json diff --git a/Demo/Sources/AppDelegate.swift b/Demo/DemoApp/Sources/AppDelegate.swift similarity index 100% rename from Demo/Sources/AppDelegate.swift rename to Demo/DemoApp/Sources/AppDelegate.swift diff --git a/Demo/DemoApp/Sources/DogAPI.swift b/Demo/DemoApp/Sources/DogAPI.swift new file mode 100644 index 0000000..f89527d --- /dev/null +++ b/Demo/DemoApp/Sources/DogAPI.swift @@ -0,0 +1,32 @@ +// DogAPI.swift + +import Foundation + +struct DogAPI { + + let baseURL: URL + let session: URLSession = .shared + let shockRecorder: ShockRecorder? + + init(baseURL: URL, shockRecorder: ShockRecorder?) { + self.baseURL = baseURL + self.shockRecorder = shockRecorder + } + + private func buildBreedsImage(value: String) -> URLRequest { + let path = "/api/breeds/image/\(value)" + let url = baseURL.appendingPathComponent(path) + var request: URLRequest = URLRequest(url: url) + request.allHTTPHeaderFields = ["Accept": "application/json"] + request.httpMethod = "GET" + return request + } + + @available(iOS 15.0.0, *) + func breedsImage(value: String) async throws -> String? { + let request = buildBreedsImage(value: value) + let (data, response) = try await session.data(for: request, delegate: nil) + shockRecorder?.writeData(request: request, response: response, data: data) + return String(data: data, encoding: .utf8) + } +} diff --git a/Demo/DemoApp/Sources/MockRoutesManager.swift b/Demo/DemoApp/Sources/MockRoutesManager.swift new file mode 100644 index 0000000..c9e8d59 --- /dev/null +++ b/Demo/DemoApp/Sources/MockRoutesManager.swift @@ -0,0 +1,113 @@ +// MockRoutesManager.swift + +import Foundation +import Shock + +class MockRoutesManager { + + private var routesDictionary: [Int: MockHTTPRoute] = [:] + + private let dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd-HH-mm-ss" + return dateFormatter + }() + + func routeExists(response: URLResponse) -> Bool { + guard let url = response.url else { + return false + } + let urlPath = url.path + return routesDictionary.contains { route in + guard route.value.urlPath == urlPath else { + return false + } + return url.queryEquals(parameters: route.value.query) + } + } + + func fileName(url: URL, method: String, index: Int) -> String { + let timestamp = dateFormatter.string(from: Date()) + let prefix = String(format: "%03d", index) + let path = url.pathComponents.dropFirst().joined(separator: "_") + let components = URLComponents(url: url, resolvingAgainstBaseURL: true) + let queryItems = components?.queryItems?.compactMap { item -> String? in + guard let value = item.value else { return nil } + return "\(item.name)__\(value)" + } + if let query = queryItems?.joined(separator: "--") { + return "\(timestamp)_\(prefix)_\(method)_\(path)--\(query)" + } else { + return "\(timestamp)_\(prefix)_\(method)_\(path)" + } + } + + func addMockHttpRoute(request: URLRequest, response: URLResponse, at index: Int) { + guard let url = response.url, + let methodValue = request.httpMethod, + let statusCode = (response as? HTTPURLResponse)?.statusCode, + let method = MockHTTPMethod(rawValue: methodValue) else { + return + } + let name = fileName(url: url, method: method.rawValue, index: index) + let filename = "\(name).json" + let urlPath = url.path + let components = URLComponents(url: url, resolvingAgainstBaseURL: true) + + // NOTE: We assume all the query items key are different + if let queryItems = components?.queryItems { + var query: [String: String] = [:] + for item in queryItems { + if let value = item.value { + query[item.name] = value + } + } + let route = MockHTTPRoute.custom( + method: method, + urlPath: urlPath, + query: query, + requestHeaders: [:], + responseHeaders: [:], + code: statusCode, + filename: filename + ) + routesDictionary[index] = route + } else { + let route = MockHTTPRoute.simple( + method: method, + urlPath: urlPath, + code: statusCode, + filename: filename) + routesDictionary[index] = route + } + } + + func collectionRoutes() -> MockHTTPRoute { + var routes: [MockHTTPRoute] = [] + for key in routesDictionary.keys.sorted(by: { $0 < $1 }) { + guard let value = routesDictionary[key] else { + continue + } + routes.append(value) + } + return MockHTTPRoute.collection(routes: routes) + } +} + +private extension URL { + func queryEquals(parameters: [String: String]?) -> Bool { + let components = URLComponents(url: self, resolvingAgainstBaseURL: true) + guard let parameters = parameters else { + return components?.queryItems == nil + } + guard let queryItems = components?.queryItems else { + return parameters.isEmpty + } + for (key, value) in parameters { + guard queryItems.contains(where: { $0.name == key && $0.value == value}) else { + return false + } + } + return true + } +} diff --git a/Demo/Sources/MyRoutes.swift b/Demo/DemoApp/Sources/MyRoutes.swift similarity index 98% rename from Demo/Sources/MyRoutes.swift rename to Demo/DemoApp/Sources/MyRoutes.swift index 704cc56..4e89d85 100644 --- a/Demo/Sources/MyRoutes.swift +++ b/Demo/DemoApp/Sources/MyRoutes.swift @@ -56,10 +56,10 @@ class MyRoutes { } var count: Int { - return routes.count + routes.count } - func performRequest(index: Int, completion: @escaping (HTTPURLResponse, Data, Error?) -> ()) { + func performRequest(index: Int, completion: @escaping (HTTPURLResponse, Data, Error?) -> Void) { let route = routes[index] diff --git a/Demo/DemoApp/Sources/RecorderViewController.swift b/Demo/DemoApp/Sources/RecorderViewController.swift new file mode 100644 index 0000000..ed71e58 --- /dev/null +++ b/Demo/DemoApp/Sources/RecorderViewController.swift @@ -0,0 +1,90 @@ +// RecorderViewController.swift + +import UIKit + +public func shouldSaveApiResponsesOnDisk() -> Bool { + ProcessInfo.processInfo.arguments.contains("SAVE_API_RESPONSES_ON_DISK") +} + +public func isRunningUITests() -> Bool { + ProcessInfo.processInfo.arguments.contains("UI_TEST") +} + +public func testClassName() -> String? { + ProcessInfo.processInfo.environment["UI_TEST_CLASS"] +} + +public func testFunctionName() -> String? { + ProcessInfo.processInfo.environment["UI_TEST_FUNCTION"] +} + +public func testShockPort() -> String? { + ProcessInfo.processInfo.environment["UI_TEST_SHOCK_PORT"] +} + +public func testBaseURL() -> URL { + if let port = testShockPort() { + return URL(string: "http://localhost:\(port)")! + } else { + return URL(string: "http://localhost")! + } +} + +func buildBaseURL() -> URL { + guard !isRunningUITests() || shouldSaveApiResponsesOnDisk() + else { + return testBaseURL() + } + return URL(string: "https://dog.ceo")! +} + +func buildShockRecorderIfRequired() -> ShockRecorder? { + guard + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first, + shouldSaveApiResponsesOnDisk() + else { + return nil + } + let dataResponsesDirectory = documentsDirectory.appendingPathComponent("data_responses", isDirectory: true) + if isRunningUITests(), + let className = testClassName(), + let functionName = testFunctionName() { + return ShockRecorder( + outputDirectory: dataResponsesDirectory, + outputConfig: UITestOutputConfig(className: className, functionName: functionName) + ) + } + return ShockRecorder(outputDirectory: dataResponsesDirectory, outputConfig: nil) +} + +class RecorderViewController: UIViewController { + + @IBOutlet var scrollView: UIScrollView! + @IBOutlet var label: UILabel! + @IBOutlet var button: UIButton! + + let shockRecorder = buildShockRecorderIfRequired() + let baseURL: URL = buildBaseURL() + lazy var apiClient = DogAPI(baseURL: baseURL, shockRecorder: shockRecorder) + + override func viewDidLoad() { + super.viewDidLoad() + + button.layer.cornerRadius = 8.0 + scrollView.layer.cornerRadius = 8.0 + label.accessibilityIdentifier = "RecorderViewController.label" + } + + @IBAction func performRequest(sender: UIButton) { + Task { @MainActor in + do { + let value = try await apiClient.breedsImage(value: "random") + self.label.text = value + } catch { + self.label.text = error.localizedDescription + } + self.label.sizeToFit() + self.scrollView.contentOffset = CGPoint(x: 0, y: 0) + } + } +} diff --git a/Demo/Sources/SceneDelegate.swift b/Demo/DemoApp/Sources/SceneDelegate.swift similarity index 97% rename from Demo/Sources/SceneDelegate.swift rename to Demo/DemoApp/Sources/SceneDelegate.swift index 08f8555..4532f75 100644 --- a/Demo/Sources/SceneDelegate.swift +++ b/Demo/DemoApp/Sources/SceneDelegate.swift @@ -10,7 +10,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + guard (scene as? UIWindowScene) != nil else { return } } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/Demo/DemoApp/Sources/ShockRecorder.swift b/Demo/DemoApp/Sources/ShockRecorder.swift new file mode 100644 index 0000000..42a0dee --- /dev/null +++ b/Demo/DemoApp/Sources/ShockRecorder.swift @@ -0,0 +1,86 @@ +// ShockRecorder.swift + +import Foundation + +class ShockRecorder { + private var index = 1 + private let outputDirectory: URL + private let outputConfig: UITestOutputConfig? + internal let queue = DispatchQueue(label: "ShockRecorder", qos: .default) + + private let routeManager = MockRoutesManager() + + init(outputDirectory: URL, outputConfig: UITestOutputConfig?) { + self.outputConfig = outputConfig + if let outputConfig = outputConfig { + self.outputDirectory = outputConfig.appendRelativePath(url: outputDirectory) + print("[ShockRecorder] function: \(outputConfig.functionName) - \(outputConfig.functionNameMD5)") + cleanDataFolder() + } else { + self.outputDirectory = outputDirectory + } + } + + private func cleanDataFolder() { + do { + try FileManager.default.removeItem(at: outputDirectory) + } catch { + } + } + + func writeData(request: URLRequest, response: URLResponse, data: Data?) { + guard let data = data, + let url = request.url, + let method = request.httpMethod else { + return + } + queue.async { [weak self] in + // TODO: Shock doesn't support different responses with the same url.path + guard let self, + !self.routeManager.routeExists(response: response), + let index = self.writeDataToFile(data, url: url, method: method) else { + return + } + self.routeManager.addMockHttpRoute(request: request, response: response, at: index) + self.writeConfigToFile() + } + } + + @discardableResult + private func writeDataToFile(_ data: Data, url: URL, method: String) -> Int? { + do { + try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true, attributes: nil) + let name = routeManager.fileName(url: url, method: method, index: index) + let outputFile = outputDirectory + .appendingPathComponent(name) + .appendingPathExtension("json") + let output = String(data: data, encoding: .utf8) + try output?.write(to: outputFile, atomically: true, encoding: .utf8) + print("[ShockRecorder] filePath: \(outputFile)") + index += 1 + return index - 1 + } + catch { + print("[ShockRecorder] error: \(error)") + } + return nil + } + + func writeConfigToFile() { + guard let outputConfig = outputConfig else { + return + } + + do { + try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true, attributes: nil) + let outputFile = outputConfig.configFileName(url: outputDirectory) + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes, .prettyPrinted] + let collectionRoute = routeManager.collectionRoutes() + let data = try encoder.encode(collectionRoute) + try data.write(to: outputFile, options: [.atomic]) + } catch { + print("[ShockRecorder] error: \(error)") + } + } +} diff --git a/Demo/DemoApp/Sources/UITestOutputConfig.swift b/Demo/DemoApp/Sources/UITestOutputConfig.swift new file mode 100644 index 0000000..1d79674 --- /dev/null +++ b/Demo/DemoApp/Sources/UITestOutputConfig.swift @@ -0,0 +1,31 @@ +// UITestOutputConfig.swift + +import Foundation +import CryptoKit + +struct UITestOutputConfig { + let className: String + let functionName: String + let basePath: String = "UITests" + + var functionNameMD5: String { + let digest = Insecure.MD5.hash(data: functionName.data(using: .utf8) ?? Data()) + return digest.map { + String(format: "%02hhx", $0) + }.joined() + } + + func appendRelativePath(url: URL) -> URL { + var urlWithPath = url + urlWithPath.appendPathComponent(basePath, isDirectory: true) + urlWithPath.appendPathComponent(className, isDirectory: true) + urlWithPath.appendPathComponent(functionNameMD5, isDirectory: true) + return urlWithPath + } + + func configFileName(url: URL?) -> URL { + var urlWithPath = url ?? URL(fileURLWithPath: "") + urlWithPath.appendPathExtension("json") + return urlWithPath + } +} diff --git a/Demo/Sources/ViewController.swift b/Demo/DemoApp/Sources/ViewController.swift similarity index 94% rename from Demo/Sources/ViewController.swift rename to Demo/DemoApp/Sources/ViewController.swift index 056ed78..6891ef7 100644 --- a/Demo/Sources/ViewController.swift +++ b/Demo/DemoApp/Sources/ViewController.swift @@ -43,26 +43,23 @@ class ViewController: UIViewController { self.scrollView.contentOffset = CGPoint(x: 0, y: 0) } } - } - } extension ViewController: UIPickerViewDataSource { func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 1 + 1 } func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - return routes.count + routes.count } } extension ViewController: UIPickerViewDelegate { func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { - return routes.nameOfRoute(at: row) + routes.nameOfRoute(at: row) } - } diff --git a/Demo/Resources/Base.lproj/Main.storyboard b/Demo/Resources/Base.lproj/Main.storyboard deleted file mode 100644 index 0b744d2..0000000 --- a/Demo/Resources/Base.lproj/Main.storyboard +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Demo/Sources/Info.plist b/Demo/Sources/Info.plist deleted file mode 100644 index dd3c9af..0000000 --- a/Demo/Sources/Info.plist +++ /dev/null @@ -1,25 +0,0 @@ - - - - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - - diff --git a/Demo/UITests/Info.plist b/Demo/UITests/Info.plist new file mode 100644 index 0000000..26b175d --- /dev/null +++ b/Demo/UITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/Demo/UITests/Resources/RecordedMocks/DemoAppUITests/320d4092848f48cbfd0df5fd94b4af22.json b/Demo/UITests/Resources/RecordedMocks/DemoAppUITests/320d4092848f48cbfd0df5fd94b4af22.json new file mode 100644 index 0000000..ac6e123 --- /dev/null +++ b/Demo/UITests/Resources/RecordedMocks/DemoAppUITests/320d4092848f48cbfd0df5fd94b4af22.json @@ -0,0 +1,12 @@ +{ + "type" : "collection", + "routes" : [ + { + "code" : 200, + "method" : "GET", + "urlPath" : "/api/breeds/image/random", + "type" : "simple", + "filename" : "2023-09-15-14-46-21_001_GET_api_breeds_image_random.json" + } + ] +} diff --git a/Demo/UITests/Resources/RecordedMocks/DemoAppUITests/320d4092848f48cbfd0df5fd94b4af22/2023-09-15-14-46-21_001_GET_api_breeds_image_random.json b/Demo/UITests/Resources/RecordedMocks/DemoAppUITests/320d4092848f48cbfd0df5fd94b4af22/2023-09-15-14-46-21_001_GET_api_breeds_image_random.json new file mode 100644 index 0000000..1768a3e --- /dev/null +++ b/Demo/UITests/Resources/RecordedMocks/DemoAppUITests/320d4092848f48cbfd0df5fd94b4af22/2023-09-15-14-46-21_001_GET_api_breeds_image_random.json @@ -0,0 +1 @@ +{"message":"https:\/\/images.dog.ceo\/breeds\/mastiff-english\/3.jpg","status":"success"} \ No newline at end of file diff --git a/Demo/UITests/Sources/DemoAppUITests.swift b/Demo/UITests/Sources/DemoAppUITests.swift new file mode 100644 index 0000000..9d96d90 --- /dev/null +++ b/Demo/UITests/Sources/DemoAppUITests.swift @@ -0,0 +1,79 @@ +// DemoAppUITests.swift + +import XCTest +import Shock + +enum DemoAppTestsError: Error { + case resourceNotFound +} + +final class DemoAppUITests: XCTestCase { + + private var mockServer: MockServer! + private var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + continueAfterFailure = false + } + + override func tearDown() { + app.terminate() + super.tearDown() + } + + func setupMockHTTPRoute(route: MockHTTPRoute) { + if mockServer != nil { mockServer.stop() } + mockServer = MockServer(portRange: 9090...9099, bundle: Bundle(for: DemoAppUITests.self)) + mockServer.start() + mockServer.setup(route: route) + } + + func loadHTTPMockRoutes(outputConfig: UITestOutputConfig, mockAuthentication: Bool = true) throws -> MockHTTPRoute { + do { + let decoder = JSONDecoder() + guard let url = Bundle(for: type(of: self)).url(forResource: outputConfig.functionNameMD5, withExtension: "json") else { + throw DemoAppTestsError.resourceNotFound + } + let data = try Data(contentsOf: url) + return try decoder.decode(MockHTTPRoute.self, from: data) + } catch { + return MockHTTPRoute.collection(routes: []) + } + } + + func testShockRecorder() throws { + + // Setup Shock + let className = String(describing: Self.self) + + let outputConfig = UITestOutputConfig( + className: className, + functionName: #function + ) + let route = try loadHTTPMockRoutes(outputConfig: outputConfig) + setupMockHTTPRoute(route: route) + + // Setup App + // Uncomment the following line if you want to call the API and record the response +// app.launchArguments.append("SAVE_API_RESPONSES_ON_DISK") + + app.launchArguments.append("UI_TEST") + app.launchEnvironment["UI_TEST_CLASS"] = className + app.launchEnvironment["UI_TEST_FUNCTION"] = #function + app.launchEnvironment["UI_TEST_SHOCK_PORT"] = "\(mockServer.selectedHTTPPort)" + + app.launch() + + // Run Tests + app.staticTexts["Test Recorder"].tap() + app.staticTexts["Perform Request"].tap() + + let scrollView = app.scrollViews.firstMatch + XCTAssertTrue(scrollView.waitForExistence(timeout: 3)) + let text = scrollView.staticTexts["RecorderViewController.label"] + text.tap() + XCTAssertTrue(text.label.contains("mastiff-english")) + } +} diff --git a/Demo/UITests/Sources/UITestOutputConfig.swift b/Demo/UITests/Sources/UITestOutputConfig.swift new file mode 100644 index 0000000..1d79674 --- /dev/null +++ b/Demo/UITests/Sources/UITestOutputConfig.swift @@ -0,0 +1,31 @@ +// UITestOutputConfig.swift + +import Foundation +import CryptoKit + +struct UITestOutputConfig { + let className: String + let functionName: String + let basePath: String = "UITests" + + var functionNameMD5: String { + let digest = Insecure.MD5.hash(data: functionName.data(using: .utf8) ?? Data()) + return digest.map { + String(format: "%02hhx", $0) + }.joined() + } + + func appendRelativePath(url: URL) -> URL { + var urlWithPath = url + urlWithPath.appendPathComponent(basePath, isDirectory: true) + urlWithPath.appendPathComponent(className, isDirectory: true) + urlWithPath.appendPathComponent(functionNameMD5, isDirectory: true) + return urlWithPath + } + + func configFileName(url: URL?) -> URL { + var urlWithPath = url ?? URL(fileURLWithPath: "") + urlWithPath.appendPathExtension("json") + return urlWithPath + } +} diff --git a/Demo/UITests/UITests.xctestplan b/Demo/UITests/UITests.xctestplan new file mode 100644 index 0000000..99909c1 --- /dev/null +++ b/Demo/UITests/UITests.xctestplan @@ -0,0 +1,33 @@ +{ + "configurations" : [ + { + "id" : "A5EDFAE4-75A3-4F8F-AE33-83A33F456EC9", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "targetForVariableExpansion" : { + "containerPath" : "container:DemoApp.xcodeproj", + "identifier" : "607FACCF1AFB9204008FA782", + "name" : "DemoApp" + } + }, + "testTargets" : [ + { + "skippedTests" : [ + "TestPersonalizedHelpHomeUnLog\/testClickOnSearch()", + "TestPersonalizedHelpHomeUnLog\/testClickOnSection()" + ], + "target" : { + "containerPath" : "container:DemoApp.xcodeproj", + "identifier" : "8F97EC1C6B99739E5669DEEE", + "name" : "UITests" + } + } + ], + "version" : 1 +} diff --git a/Framework/Sources/Extensions/String+PathMatching.swift b/Framework/Sources/Extensions/String+PathMatching.swift index e4ac826..5f5d8ed 100644 --- a/Framework/Sources/Extensions/String+PathMatching.swift +++ b/Framework/Sources/Extensions/String+PathMatching.swift @@ -9,7 +9,7 @@ extension String { } private func pathMatches(_ other: String, tokenPrefix: Character) -> Bool { - let bothTemplates = self.contains() { $0 == tokenPrefix } && other.contains() { $0 == tokenPrefix } + let bothTemplates = self.contains { $0 == tokenPrefix } && other.contains { $0 == tokenPrefix } let parts = self.split(separator: "/") let otherParts = other.split(separator: "/") guard parts.count == otherParts.count else { return false } diff --git a/Framework/Sources/Middleware/ClosureMiddleware.swift b/Framework/Sources/Middleware/ClosureMiddleware.swift index 80f0ffd..2166f20 100644 --- a/Framework/Sources/Middleware/ClosureMiddleware.swift +++ b/Framework/Sources/Middleware/ClosureMiddleware.swift @@ -17,5 +17,4 @@ public struct ClosureMiddleware: Middleware { public func execute(withContext context: MiddlewareContext) { closure(context.requestContext, context.responseContext, context.next) } - } diff --git a/Framework/Sources/Middleware/Middleware.swift b/Framework/Sources/Middleware/Middleware.swift index 1748443..32cdf1a 100644 --- a/Framework/Sources/Middleware/Middleware.swift +++ b/Framework/Sources/Middleware/Middleware.swift @@ -9,10 +9,10 @@ public protocol MiddlewareRequestContext: MockHttpRequest { var path: String { get } var queryParams: [(String, String)] { get } var method: String { get } - var headers: [String : String] { get } + var headers: [String: String] { get } var body: [UInt8] { get } var address: String? { get } - var params: [String : String] { get } + var params: [String: String] { get } } public protocol MiddlewareResponseContext: AnyObject { @@ -38,10 +38,10 @@ class MiddlewareService { let path: String let queryParams: [(String, String)] let method: String - let headers: [String : String] + let headers: [String: String] let body: [UInt8] let address: String? - let params: [String : String] + let params: [String: String] init(request: MockNIOHTTPRequest) { self.path = request.path @@ -56,8 +56,8 @@ class MiddlewareService { private class _MiddlewareResponseContext: MiddlewareResponseContext { var statusCode: Int = 0 - var headers: [String : String] = [:] - var responseBody: Data? = nil + var headers: [String: String] = [:] + var responseBody: Data? } private struct _MiddlewareContext: MiddlewareContext { @@ -70,7 +70,7 @@ class MiddlewareService { private let middleware: [Middleware] private let notFoundHandler: HandlerClosure? - public init(middleware: [Middleware], notFoundHandler: HandlerClosure?) { + init(middleware: [Middleware], notFoundHandler: HandlerClosure?) { self.middleware = middleware self.notFoundHandler = notFoundHandler } diff --git a/Framework/Sources/Middleware/MockRoutesMiddleware.swift b/Framework/Sources/Middleware/MockRoutesMiddleware.swift index b823a53..afeffdd 100644 --- a/Framework/Sources/Middleware/MockRoutesMiddleware.swift +++ b/Framework/Sources/Middleware/MockRoutesMiddleware.swift @@ -6,9 +6,9 @@ struct MockRoutesMiddleware: Middleware { let router: MockNIOHTTPRouter - let responseFactory: ResponseFactory + let responseFactory: MockHTTPResponseFactory - init(router: MockNIOHTTPRouter, responseFactory: ResponseFactory) { + init(router: MockNIOHTTPRouter, responseFactory: MockHTTPResponseFactory) { self.router = router self.responseFactory = responseFactory } diff --git a/Framework/Sources/MockHTTPMethod.swift b/Framework/Sources/MockHTTPMethod.swift index e91442a..c71836a 100644 --- a/Framework/Sources/MockHTTPMethod.swift +++ b/Framework/Sources/MockHTTPMethod.swift @@ -3,10 +3,10 @@ import Foundation public enum MockHTTPMethod: String { - case get = "GET" - case head = "HEAD" - case post = "POST" - case put = "PUT" - case patch = "PATCH" - case delete = "DELETE" + case get = "GET" + case head = "HEAD" + case post = "POST" + case put = "PUT" + case patch = "PATCH" + case delete = "DELETE" } diff --git a/Framework/Sources/MockHTTPResponseFactory.swift b/Framework/Sources/MockHTTPResponseFactory.swift index 45c993e..645a48b 100644 --- a/Framework/Sources/MockHTTPResponseFactory.swift +++ b/Framework/Sources/MockHTTPResponseFactory.swift @@ -1,4 +1,4 @@ -// HTTPResponseFactory.swift +// MockHTTPResponseFactory.swift import Foundation #if canImport(GRMustache) @@ -8,7 +8,7 @@ typealias Template = GRMustacheTemplate import Mustache #endif -class ResponseFactory { +class MockHTTPResponseFactory { class TemplateHelper { @@ -67,5 +67,4 @@ class ResponseFactory { guard let url = _url else { return nil } return try? Data(contentsOf: url) } - } diff --git a/Framework/Sources/MockHTTPRoute+Codable.swift b/Framework/Sources/MockHTTPRoute+Codable.swift index d721d5d..05a12fd 100644 --- a/Framework/Sources/MockHTTPRoute+Codable.swift +++ b/Framework/Sources/MockHTTPRoute+Codable.swift @@ -241,7 +241,7 @@ enum TemplateParameter: Hashable { self = .dictionary(dict) return } - if let _ = value as? NSNull { + if value as? NSNull != nil { self = .nil return } diff --git a/Framework/Sources/MockHTTPRoute.swift b/Framework/Sources/MockHTTPRoute.swift index 62847ca..00b391d 100644 --- a/Framework/Sources/MockHTTPRoute.swift +++ b/Framework/Sources/MockHTTPRoute.swift @@ -126,10 +126,10 @@ public enum MockHTTPRoute { } } - var templateInfo: [String: Any]? { + var templateInfo: [String: Any]? { switch self { case .template(_, _, _, _, let templateInfo): - return templateInfo as [String : Any] + return templateInfo as [String: Any] default: return nil } @@ -143,7 +143,6 @@ public enum MockHTTPRoute { return nil } } - } /// The philosophy for Equatable/Hashable `MockHTTPRoute` is anything in the request @@ -178,7 +177,7 @@ extension MockHTTPRoute: Equatable { return false } - private static func headers(_ lhs: [String:String], contains rhs: [String:String]) -> Bool { + private static func headers(_ lhs: [String: String], contains rhs: [String: String]) -> Bool { guard !(lhs.isEmpty && rhs.isEmpty) else { return true } var bigger = lhs var smaller = rhs @@ -188,7 +187,7 @@ extension MockHTTPRoute: Equatable { } guard !smaller.isEmpty else { return true } for outer in smaller { - let result = bigger.contains() { (key: String, value: String) in + let result = bigger.contains { (key: String, value: String) in key.lowercased() == outer.key.lowercased() && value.lowercased() == outer.value.lowercased() } if result { @@ -198,7 +197,7 @@ extension MockHTTPRoute: Equatable { return false } - private static func queryParamsMatch(lhs: [String:String], rhs: [String:String]) -> Bool { + private static func queryParamsMatch(lhs: [String: String], rhs: [String: String]) -> Bool { if lhs.count != rhs.count { return false } @@ -211,7 +210,7 @@ extension MockHTTPRoute: Equatable { return true } - public func matches(method: MockHTTPMethod, path: String, params: [String:String], headers: [String:String]) -> Bool { + public func matches(method: MockHTTPMethod, path: String, params: [String: String], headers: [String: String]) -> Bool { guard !method.rawValue.isEmpty else { return false } guard !path.isEmpty else { return false } switch self { diff --git a/Framework/Sources/MockHTTPServerProtocols.swift b/Framework/Sources/MockHTTPServerProtocols.swift index 0392813..ceff6c9 100644 --- a/Framework/Sources/MockHTTPServerProtocols.swift +++ b/Framework/Sources/MockHTTPServerProtocols.swift @@ -10,9 +10,8 @@ protocol MockHttpRouter { protocol MockHttpServer { var notFoundHandler: HandlerClosure? { get set } func register(route: MockHTTPRoute, handler: HandlerClosure?) - func start(_ port: Int, forceIPv4: Bool, priority: DispatchQoS.QoSClass) throws -> Void + func start(_ port: Int, forceIPv4: Bool, priority: DispatchQoS.QoSClass) throws func stop() } public protocol MockHttpRequest: CacheableRequest {} - diff --git a/Framework/Sources/MockServer.swift b/Framework/Sources/MockServer.swift index 00833d2..3f0ec17 100644 --- a/Framework/Sources/MockServer.swift +++ b/Framework/Sources/MockServer.swift @@ -1,4 +1,4 @@ -// MockAPI.swift +// MockServer.swift import Foundation @@ -9,21 +9,28 @@ public class MockServer { private var httpServer: MockNIOHttpServer private var socketServer: MockNIOSocketServer? - private let responseFactory: ResponseFactory + private let responseFactory: MockHTTPResponseFactory public var selectedHTTPPort = 0 public var selectedSocketPort = 0 public var loggingClosure: ((String?) -> Void)? + public enum MissingRouteHandlingPolicy { + case assert + case return404 + } + public var missingRouteHandlingPolicy: MissingRouteHandlingPolicy + public convenience init(port: Int = 9000, bundle: Bundle = Bundle.main) { self.init(portRange: port...port, bundle: bundle) } public init(portRange: ClosedRange, bundle: Bundle = Bundle.main) { self.portRange = portRange - self.responseFactory = ResponseFactory(bundle: bundle) + self.responseFactory = MockHTTPResponseFactory(bundle: bundle) self.httpServer = MockNIOHttpServer(responseFactory: self.responseFactory) + self.missingRouteHandlingPolicy = .return404 } // MARK: Server managements @@ -66,16 +73,22 @@ Run `netstat -anptcp | grep LISTEN` to check which ports are in use.") } /// Indicates whether a 404 status should be sent for requests that do - /// not have a matching route + /// not have a matching route, alternatively an assertionFailure. public var shouldSendNotFoundForMissingRoutes: Bool { get { - httpServer.notFoundHandler != nil + httpServer.notFoundHandler != nil } set { if newValue { - httpServer.notFoundHandler = { _, response in - response.statusCode = 404 - response.responseBody = nil + httpServer.notFoundHandler = { [weak self] request, response in + guard let self = self else { return } + switch self.missingRouteHandlingPolicy { + case .assert: + assertionFailure("Not handled: \(request.method) \(request.path)") + case .return404: + response.statusCode = 404 + response.responseBody = nil + } } } else { httpServer.notFoundHandler = nil @@ -84,7 +97,7 @@ Run `netstat -anptcp | grep LISTEN` to check which ports are in use.") } public var hostURL: String { - return "http://localhost:\(selectedHTTPPort)" + "http://localhost:\(selectedHTTPPort)" } // MARK: Mock setup @@ -98,14 +111,13 @@ Run `netstat -anptcp | grep LISTEN` to check which ports are in use.") default: break } - - guard let _ = route.method, let _ = route.urlPath else { + guard route.method != nil, route.urlPath != nil else { self.loggingClosure?("ERROR: route was missing a field") return } - httpServer.register(route: route) { request, response in + httpServer.register(route: route) { _, response in switch route { case .redirect(_, let destination): @@ -139,7 +151,6 @@ Run `netstat -anptcp | grep LISTEN` to check which ports are in use.") response.responseBody = data } - } public func setupSocket(route: MockSocketRoute) { @@ -156,12 +167,11 @@ Run `netstat -anptcp | grep LISTEN` to check which ports are in use.") public func add(middleware: Middleware) { httpServer.add(middleware: middleware) } - } // MARK: Utils -fileprivate func dictionary(from query: [(String, String)]) -> [String: String] { +private func dictionary(from query: [(String, String)]) -> [String: String] { var dict = [String: String]() query.forEach { dict[$0.0] = $0.1 } return dict @@ -188,5 +198,4 @@ fileprivate extension Dictionary where Key == String, Value == String { } return true } - } diff --git a/Framework/Sources/NIO/MockNIOBaseServer.swift b/Framework/Sources/NIO/MockNIOBaseServer.swift index 51e5694..c39d37b 100644 --- a/Framework/Sources/NIO/MockNIOBaseServer.swift +++ b/Framework/Sources/NIO/MockNIOBaseServer.swift @@ -1,4 +1,4 @@ -// MockNIOServer.swift +// MockNIOBaseServer.swift import Foundation import NIO @@ -19,7 +19,7 @@ class MockNIOBaseServer { self.threadPool = NIOThreadPool(numberOfThreads: 6) } - func start(_ port: Int, childChannelInitializer: @escaping (Channel) -> EventLoopFuture) throws -> Void { + func start(_ port: Int, childChannelInitializer: @escaping (Channel) -> EventLoopFuture) throws { threadPool.start() let socketBootstrap = ServerBootstrap(group: group) diff --git a/Framework/Sources/NIO/MockNIOHTTPHandler.swift b/Framework/Sources/NIO/MockNIOHTTPHandler.swift index dd81a07..09ef5ad 100644 --- a/Framework/Sources/NIO/MockNIOHTTPHandler.swift +++ b/Framework/Sources/NIO/MockNIOHTTPHandler.swift @@ -10,12 +10,12 @@ class MockNIOHTTPHandler { private var httpRequest: HTTPRequestHead? private var handlerRequest: MockNIOHTTPRequest? - private var responseFactory: ResponseFactory + private var responseFactory: MockHTTPResponseFactory private var router: MockNIOHTTPRouter private var middleware: [Middleware] private var notFoundHandler: HandlerClosure? - init(responseFactory: ResponseFactory, + init(responseFactory: MockHTTPResponseFactory, router: MockNIOHTTPRouter, middleware: [Middleware], notFoundHandler: HandlerClosure?) { @@ -60,7 +60,7 @@ class MockNIOHTTPHandler { let body = [UInt8]() let address = url.host var params = [String: String]() - var queryParams = [(String, String)]() + var queryParams = [(String, String)]() if let queryItems = url.queryItems { params = queryItems.reduce(into: [String: String](), { $0[$1.name] = $1.value }) queryParams = queryItems.reduce(into: [(String, String)](), { $0.append(($1.name, $1.value ?? "")) }) @@ -127,7 +127,7 @@ extension MockNIOHTTPHandler: ChannelInboundHandler { guard var handlerRequest = self.handlerRequest else { return } handlerRequest.body += bytes.readBytes(length: bytes.readableBytes) ?? [] self.handlerRequest = handlerRequest - case .end(_): + case .end: guard self.httpRequest != nil else { return } guard let handlerRequest = self.handlerRequest else { return } guard let version = self.httpRequest?.version else { return } diff --git a/Framework/Sources/NIO/MockNIOHTTPServer.swift b/Framework/Sources/NIO/MockNIOHTTPServer.swift index 8a1c518..003cf46 100644 --- a/Framework/Sources/NIO/MockNIOHTTPServer.swift +++ b/Framework/Sources/NIO/MockNIOHTTPServer.swift @@ -7,19 +7,19 @@ import NIOHTTP1 /// SwiftNIO implementation of mock HTTP server class MockNIOHttpServer: MockNIOBaseServer, MockHttpServer { - private let responseFactory: ResponseFactory + private let responseFactory: MockHTTPResponseFactory private var httpHandler: MockNIOHTTPHandler? private var router = MockNIOHTTPRouter() private var middleware = [Middleware]() private var routeMiddleware: MockRoutesMiddleware? var notFoundHandler: HandlerClosure? - init(responseFactory: ResponseFactory) { + init(responseFactory: MockHTTPResponseFactory) { self.responseFactory = responseFactory super.init() } - func start(_ port: Int, forceIPv4: Bool, priority: DispatchQoS.QoSClass) throws -> Void { + func start(_ port: Int, forceIPv4: Bool, priority: DispatchQoS.QoSClass) throws { try start(port) { (channel) -> EventLoopFuture in channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { self.httpHandler = MockNIOHTTPHandler(responseFactory: self.responseFactory, @@ -52,10 +52,10 @@ struct MockNIOHTTPRequest: MockHttpRequest { var path: String var queryParams: [(String, String)] var method: String - var headers: [String : String] + var headers: [String: String] var body: [UInt8] var address: String? - var params: [String : String] + var params: [String: String] } struct RouteHandlerMapping { @@ -70,13 +70,11 @@ struct MockNIOHTTPRouter: MockHttpRouter { !routes.isEmpty } - func handlerForMethod(_ method: String, path: String, params: [String:String], headers: [String:String]) -> HandlerClosure? { + func handlerForMethod(_ method: String, path: String, params: [String: String], headers: [String: String]) -> HandlerClosure? { guard let httpMethod = MockHTTPMethod(rawValue: method.uppercased()) else { return nil } let methodRoutes = routes[httpMethod] ?? [RouteHandlerMapping]() - for mapping in methodRoutes { - if mapping.route.matches(method: httpMethod, path: path, params: params, headers: headers) { - return mapping.handler - } + for mapping in methodRoutes where mapping.route.matches(method: httpMethod, path: path, params: params, headers: headers) { + return mapping.handler } return nil } diff --git a/Framework/Sources/NIO/MockNIOSocketHandler.swift b/Framework/Sources/NIO/MockNIOSocketHandler.swift index 11bdd9e..6cbf5a3 100644 --- a/Framework/Sources/NIO/MockNIOSocketHandler.swift +++ b/Framework/Sources/NIO/MockNIOSocketHandler.swift @@ -4,12 +4,12 @@ import Foundation import NIO class MockNIOSocketHandler: ChannelInboundHandler { - public typealias InboundIn = ByteBuffer - public typealias OutboundOut = ByteBuffer - public typealias LoggingClosure = (String?) -> Void - public typealias SocketDataHandler = (Data, LoggingClosure?) -> Void + typealias InboundIn = ByteBuffer + typealias OutboundOut = ByteBuffer + typealias LoggingClosure = (String?) -> Void + typealias SocketDataHandler = (Data, LoggingClosure?) -> Void - private var received: Data? = nil + private var received: Data? private var loggingClosure: LoggingClosure? private var dataHandler: SocketDataHandler? @@ -19,7 +19,7 @@ class MockNIOSocketHandler: ChannelInboundHandler { self.loggingClosure = loggingClosure } - public func channelRead(context: ChannelHandlerContext, data: NIOAny) { + func channelRead(context: ChannelHandlerContext, data: NIOAny) { var buffer = unwrapInboundIn(data) let readableBytes = buffer.readableBytes if readableBytes > 0, let received = buffer.readBytes(length: readableBytes) { @@ -31,7 +31,7 @@ class MockNIOSocketHandler: ChannelInboundHandler { } } - public func channelReadComplete(context: ChannelHandlerContext) { + func channelReadComplete(context: ChannelHandlerContext) { if let data = self.received { dataHandler?(data, loggingClosure) if let separator = "\n".data(using: String.Encoding.utf8)?.first { @@ -51,7 +51,7 @@ class MockNIOSocketHandler: ChannelInboundHandler { self.received = nil } - public func errorCaught(context: ChannelHandlerContext, error: Error) { + func errorCaught(context: ChannelHandlerContext, error: Error) { context.close(promise: nil) } } diff --git a/Framework/Sources/NIO/MockNIOSocketServer.swift b/Framework/Sources/NIO/MockNIOSocketServer.swift index 80a960e..54bfb7e 100644 --- a/Framework/Sources/NIO/MockNIOSocketServer.swift +++ b/Framework/Sources/NIO/MockNIOSocketServer.swift @@ -6,13 +6,13 @@ import NIOHTTP1 class MockNIOSocketServer: MockNIOBaseServer { - public var socketDataHandler: MockNIOSocketHandler.SocketDataHandler? - public var loggingClosure: ((String?) -> Void)? + var socketDataHandler: MockNIOSocketHandler.SocketDataHandler? + var loggingClosure: ((String?) -> Void)? - func start(_ port: Int) throws -> Void { + func start(_ port: Int) throws { try start(port) { (channel) -> EventLoopFuture in // Ensure we don't read faster than we can write by adding the BackPressureHandler into the pipeline. - channel.pipeline.addHandler(BackPressureHandler()).flatMap { v in + channel.pipeline.addHandler(BackPressureHandler()).flatMap { _ in channel.pipeline.addHandler(MockNIOSocketHandler(dataHandler: self.socketDataHandler, loggingClosure: self.loggingClosure)) } } diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 6fd787d..0000000 --- a/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -ruby '3.0.2' - -source 'https://rubygems.org' - -gem 'cocoapods', '1.11.2' diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 54f5395..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,100 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.5) - rexml - activesupport (6.1.6) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) - algoliasearch (1.27.5) - httpclient (~> 2.8, >= 2.8.3) - json (>= 1.5.1) - atomos (0.1.3) - claide (1.1.0) - cocoapods (1.11.2) - addressable (~> 2.8) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.11.2) - cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.4.0, < 2.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) - cocoapods-try (>= 1.1.0, < 2.0) - colored2 (~> 3.1) - escape (~> 0.0.4) - fourflusher (>= 2.3.0, < 3.0) - gh_inspector (~> 1.0) - molinillo (~> 0.8.0) - nap (~> 1.0) - ruby-macho (>= 1.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.11.2) - activesupport (>= 5.0, < 7) - addressable (~> 2.8) - algoliasearch (~> 1.0) - concurrent-ruby (~> 1.1) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - netrc (~> 0.11) - public_suffix (~> 4.0) - typhoeus (~> 1.0) - cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.1) - cocoapods-trunk (1.6.0) - nap (>= 0.8, < 2.0) - netrc (~> 0.11) - cocoapods-try (1.2.0) - colored2 (3.1.2) - concurrent-ruby (1.1.10) - escape (0.0.4) - ethon (0.15.0) - ffi (>= 1.15.0) - ffi (1.15.5) - fourflusher (2.3.1) - fuzzy_match (2.0.4) - gh_inspector (1.1.3) - httpclient (2.8.3) - i18n (1.10.0) - concurrent-ruby (~> 1.0) - json (2.6.2) - minitest (5.16.1) - molinillo (0.8.0) - nanaimo (0.3.0) - nap (1.1.0) - netrc (0.11.0) - public_suffix (4.0.7) - rexml (3.2.5) - ruby-macho (2.5.1) - typhoeus (1.4.0) - ethon (>= 0.9.0) - tzinfo (2.0.4) - concurrent-ruby (~> 1.0) - xcodeproj (1.22.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) - zeitwerk (2.6.0) - -PLATFORMS - ruby - -DEPENDENCIES - cocoapods (= 1.11.2) - -RUBY VERSION - ruby 3.0.2p107 - -BUNDLED WITH - 2.3.7 diff --git a/Package.resolved b/Package.resolved index a8b9c83..722a2c1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,43 +1,41 @@ { - "object": { - "pins": [ - { - "package": "Mustache", - "repositoryURL": "https://github.com/groue/GRMustache.swift", - "state": { - "branch": null, - "revision": "671e2c5234e829c1f337c9d300bd6d9b21d1c42a", - "version": "4.0.1" - } - }, - { - "package": "JustLog", - "repositoryURL": "https://github.com/justeat/JustLog", - "state": { - "branch": null, - "revision": "b9bb34d2ae6d3d5e7d2ef678f588718ed96d643e", - "version": "4.0.2" - } - }, - { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio", - "state": { - "branch": null, - "revision": "124119f0bb12384cef35aa041d7c3a686108722d", - "version": "2.40.0" - } - }, - { - "package": "SwiftyBeaver", - "repositoryURL": "https://github.com/SwiftyBeaver/SwiftyBeaver", - "state": { - "branch": null, - "revision": "12b5acf96d98f91d50de447369bd18df74600f1a", - "version": "1.9.6" - } + "pins" : [ + { + "identity" : "grmustache.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRMustache.swift", + "state" : { + "revision" : "edbe65da33671ca1e93e0751cbbeffc893b48da8", + "version" : "4.1.0" } - ] - }, - "version": 1 + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio", + "state" : { + "revision" : "f7c46552983b06b0958a1a4c8bc5199406ae4c8a", + "version" : "2.51.0" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index cdfc841..b97dee5 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,14 @@ -// swift-tools-version: 5.5 +// swift-tools-version: 5.7 + +// This file was automatically generated by PackageGenerator and untracked +// PLEASE DO NOT EDIT MANUALLY import PackageDescription let package = Package( name: "Shock", - platforms: [.iOS(.v14)], + defaultLocalization: "en", + platforms: [.iOS(.v15)], products: [ .library( name: "Shock", @@ -12,12 +16,14 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio", - from: "2.40.0"), - .package(url: "https://github.com/groue/GRMustache.swift", - from: "4.0.1"), - .package(url: "https://github.com/justeat/JustLog", - from: "4.0.2") + .package( + url: "https://github.com/groue/GRMustache.swift", + exact: "4.1.0" + ), + .package( + url: "https://github.com/apple/swift-nio", + exact: "2.51.0" + ), ], targets: [ .target( @@ -28,16 +34,17 @@ let package = Package( .product(name: "NIOHTTP1", package: "swift-nio"), .product(name: "Mustache", package: "GRMustache.swift") ], - path: "Framework/Sources"), + path: "Framework/Sources" + ), .testTarget( - name: "UnitTests", + name: "ShockTests", dependencies: [ .byName(name: "Shock"), - .product(name: "JustLog", package: "JustLog") ], path: "Tests/Sources", resources: [ .process("Resources") - ]) + ] + ), ] ) diff --git a/README.md b/README.md index 7210a0c..985a60e 100644 --- a/README.md +++ b/README.md @@ -1,391 +1,28 @@ # Shock -[![Build Status](https://travis-ci.com/justeat/Shock.svg?branch=master)](https://travis-ci.org/justeat/Shock) -[![Version](https://img.shields.io/cocoapods/v/Shock.svg?style=flat)](http://cocoapods.org/pods/Shock) -[![License](https://img.shields.io/cocoapods/l/Shock.svg?style=flat)](http://cocoapods.org/pods/Shock) -[![Platform](https://img.shields.io/cocoapods/p/Shock.svg?style=flat)](http://cocoapods.org/pods/Shock) +An HTTP mocking framework written in Swift. -A HTTP mocking framework written in Swift. +## DemoApp -- [Just Eat Tech blog](https://tech.just-eat.com/2019/03/05/shock-better-automation-testing-for-ios/) +The DemoApp is a sample app that shows how to use the Shock framework. -## Summary +- `Test Shock`: will show how to emebed Shock in your app and how to use it to mock API calls. +- `Test ShockRecorder`: will show how to use ShockRecorder to record API calls and how to use the recorded API calls to mock API calls during UI Tests. -* ๐Ÿ˜Ž **Painless API mocking**: Shock lets you quickly and painlessly provide mock responses for web requests made by your apps. +### Recording API calls with ShockRecorder -* ๐Ÿงช **Isolated mocking**: When used with UI tests, Shock runs its server within the UI test process and stores all its responses within the UI tests target - so there is no need to pollute your app target with lots of test data and logic. +To record API calls during the demo app execution, enable the `SAVE_API_RESPONSES_ON_DISK` argument passed on launch in the `DemoApp` scheme. -* โญ๏ธ **Shock now supports parallel UI testing!**: Shock can run isolated servers in parallel test processes. See below for more details! +Once the flag is enabled, the API responses will be saved in the `data_responses` folder, you can find the path in the console logs, e.g. -* ๐Ÿ”Œ **Shock can now host a basic socket**: In addition to an HTTP server, Shock can also host a socket server for a variety of testing tasks. See below for more details! - -## Installation - -### CocoaPods - -Add the following to your podfile: - -```ruby -pod 'Shock', '~> x.y.z' -``` - -You can find the latest version on [cocoapods.org](http://cocoapods.org/pods/Shock) - -### SPM - -Copy the URL for this repo, and add the package in your project settings. - -## Mocking HTTP Requests - -Shock aims to provide a simple interface for setting up your mocks. - -Take the example below: - -```swift -class HappyPathTests: XCTestCase { - - var mockServer: MockServer! - - override func setUp() { - super.setUp() - mockServer = MockServer(port: 6789, bundle: Bundle.module) - mockServer.start() - } - - override func tearDown() { - mockServer.stop() - super.tearDown() - } - - func testExample() { - - let route: MockHTTPRoute = .simple( - method: .get, - urlPath: "/my/api/endpoint", - code: 200, - filename: "my-test-data.json" - ) - - mockServer.setup(route: route) - - /* ... Your test code ... */ - } -} ``` - -Bear in mind that you will need to replace your API endpoint hostname with 'localhost' and the port you specify in the setup method during test runs. - -e.g. ```https://localhost:{PORT}/my/api/endpoint``` - -In the case or UI tests, this is most quickly accomplished by passing a launch argument to your app that indicates which endpoint to use. For example: - -```swift -let args = ProcessInfo.processInfo.arguments -let isRunningUITests = args.contains("UITests") -let port = args["MockServerPort"] -if isRunningUITests { - apiConfiguration.setHostname("http://localhost:\(port)/") -} +[ShockRecorder] filePath: file:///Users/user/Library/Developer/CoreSimulator/Devices/BE295F5C-5D11-4C70-A74E-52AF3389F0C9/data/Containers/Data/Application/A49E2BCB-1E12-4A34-8DE8-9262742BC564/Documents/data_responses/2023-09-23-16-23-31_001_GET_api_breeds_image_random.json ``` -**Note:** ๐Ÿ‘‰ The easiest way to pass arguments from your test cases to your running app is to -use another of our wonderful open-source libraries: [AutomationTools](https://github.com/justeat/AutomationTools/) - -## Route types - -Shock provides different types of mock routes for different circumstances. -All routes are conforming to the Codable protocol and can be decoded from a JSON file. - -### Simple Route - -A simple mock is the preferred way of defining a mock route. It responds with -the contents of a JSON file in the test bundle, provided as a filename to the -mock declaration like so: - -```swift -let route: MockHTTPRoute = .simple( - method: .get, - urlPath: "/my/api/endpoint", - code: 200, - filename: "my-test-data.json" -) -``` - -JSON -```JSON -{ - "type": "simple", - "method": "GET", - "urlPath": "/my/api/endpoint", - "code": 200, - "filename" : "my-test-data.json" -} -``` - -### Custom Route - -A custom mock allows further customisation of your route definition including -the addition of query string parameters and HTTP headers. - -This gives you more control over the finer details of the requests you want your -mock to handle. - -Custom routes will try to strictly match your query and header definitions so -ensure that you add custom routes for all variations of these values. - -```swift -let route = MockHTTPRoute = .custom( - method: .get, - urlPath: "/my/api/endpoint", - query: ["queryKey": "queryValue"], - requestHeaders: ["X-Custom-Header": "custom-header-value"], - responseHeaders: ["Content-Type": "application/json"], - code: 200, - filename: "my-test-data.json" -) -``` - -JSON -```JSON -{ - "type": "custom", - "method": "GET", - "urlPath": "/my/api/endpoint", - "query": { - "queryKey": "queryValue" - }, - "requestHeaders": { - "X-Custom-Header": "custom-header-value" - }, - "responseHeaders": { - "Content-Type": "application/json" - }, - "code": 200, - "filename": "my-test-data.json" -} -``` -### Redirect Route - -Sometimes we simply want our mock to redirect to another URL. The redirect mock -allows you to return a 301 redirect to another URL or endpoint. - -```swift -let route: MockHTTPRoute = .redirect(urlPath: "/source", destination: "/destination") -``` - -JSON -```JSON -{ - "type": "redirect", - "urlPath": "/source", - "destination": "/destination" -} -``` -### Templated Route - -A templated mock allows you to build a mock response for a request at runtime. -It uses [Mustache](https://mustache.github.io/) to allow values to be built in -to your responses when you setup your mocks. - -For example, you might want a response to contain an array of items that is -variable size based on the requirements of the test. - -Check out the `/template` route in the Shock Route Tester example app for a -more comprehensive example. - -```swift -let route = MockHTTPRoute = .template( - method: .get, - urlPath: "/template", - code: 200, - filename: "my-templated-data.json", - templateInfo: [ - "list": ["Item #1", "Item #2"], - "text": "text" - ]) -) -``` - -JSON -```JSON -{ - "type": "template", - "method": "GET", - "urlPath": "/template", - "code": 200, - "filename": "my-templated-data.json", - "templateInfo": { - "list": ["Item #1", "Item #2"], - "text": "text" - } -} -``` - -### Collection - -A collection route contains an array of other mock routes. It is simply a -container for storing and organising routes for different tests. In general, -if your test uses more than one route - -Collection routes are added recursively, so a given collection route can be -included in another collection route safely. - -```swift -let firstRoute: MockHTTPRoute = .simple(method: .get, urlPath: "/route1", code: 200, filename: "data1.json") -let secondRoute: MockHTTPRoute = .simple(method: .get, urlPath: "/route2", code: 200, filename: "data2.json") -let collectionRoute: MockHTTPRoute = .collection(routes: [ firstRoute, secondRoute ]) -``` -JSON -```JSON -{ - "type": "collection", - "routes": [ - { - "type": "simple", - "method": "GET", - "urlPath": "/my/api/endpoint", - "code": 200, - "filename" : "my-test-data.json" - }, - { - "type": "simple", - "method": "GET", - "urlPath": "/my/api/endpoint2", - "code": 200, - "filename" : "my-test-data2.json" - } - ] -} -``` - -### Timeout Route - -A timeout route is useful for testing client timeout code paths. -It simply waits a configurable amount of seconds (defaulting to 120 seconds). -**Note** if you do specify your own timeout, please make sure it exceeds your -client's timeout. - -```swift -let route: MockHTTPRoute = .timeout(method: .get, urlPath: "/timeouttest") -``` -```swift -let route: MockHTTPRoute = .timeout(method: .get, urlPath: "/timeouttest", timeoutInSeconds: 5) -``` -JSON -```JSON -{ - "type": "timeout", - "method": "GET", - "urlPath": "/timeouttest", - "timeoutInSeconds": 5 -} -``` - -### Force all calls to be mocked - -In some case you might prefer to have all the calls to be mocked so that the tests can reliably run without internet connection. You can force this behaviour like so: - -``` -server.shouldSendNotFoundForMissingRoutes = true -``` - -This will send a 404 status code with an empty response body for any unrecognised paths. - -## Middleware - -Shock now support middleware! Middleware lets you use custom logic to handle a given request. - -* ๐Ÿค Middleware can be used with or without mock routes. -* โ›“ Middleware is chainable with the first middleware added receiving the context first, -passing it to the next, and so on - -### ClosureMiddleware - -The simplest way to use middleware is to add an instance of ClosureMiddleware to the server. For example: - -```swift -let myMiddleware = ClosureMiddleware { request, response, next in - if request.headers["X-Question"] == "Can I have a cup of tea?" { - response.headers["X-Answer"] = "Yes, you can!" - } - next() -} -mockServer.add(middleware: myMiddleware) -``` - -The above will look for a request header named `X-Question` and, if it is present with the -expected value, it will send back an answer in the 'X-Answer' response header. - -### Using Mock Routes and Middleware Together - -Mock routes and middleware work fine together but there are a few things worth bearing in mind: - -1. Mock routes is managed by are managed by a single middleware -2. This middleware will be added to the existing stack of middlewares _when the first mock route is added to the server_. - -For middleware such as the example above, the order of middleware won't matter. However, if you -are making changes to a part of the response that was already set by the mock routes middleware, -you may get unexpected results! - -## Socket Server - -Shock can now host a socket server in addition to the HTTP server. This is useful for cases where you need to mock -HTTP requests and a socket server. The Socket server uses familiar terminology to the HTTP server, so it has inherited -the term "route" to refer to a type of socket data handler. The API is similar to the HTTP API in that you need to create a -`MockServerRoute`, call `setupSocket` with the route and when server `start` is called a socket will be setup with -your route (assuming at least one route is registered). - -If no `MockServerRoute`s are setup, the socket server is not started. - -### Prerequisites - -The socket server can only be hosted in addition to the HTTP server, as such Shock will need a port range of -at least two ports, using the `init` method that takes a range. - -```swift -let range: ClosedRange = 10000...10010 -let server = MockServer(portRange: range, bundle: ...) -``` - -### Available routes - -There is only one route currently available for the socket server and that is `logStashEcho`. This route will setup a socket -that accepts messages being logged to [Logstash](https://www.elastic.co/logstash) and echo them back as strings. - -Here is an example of using `logStashEcho` with our [JustLog](https://github.com/justeat/JustLog) framework. - -```swift -import JustLog -import Shock - -let server = MockServer(portRange: 9090...9099, bundle: ...) -let route = MockSocketRoute.logStashEcho { (log) in - print("Received \(log)" -} -server.setupSocket(route: route) -server.start() - -let logger = Logger.shared -logger.logstashHost = "localhost" -logger.logstashPort = UInt16(server.selectedSocketPort) -logger.enableLogstashLogging = true -logger.allowUntrustedServer = true -logger.setup() - -logger.info("Hello world!") -``` - -It's worth noting that Shock is an untrusted server, so the `logger.allowUntrustedServer = true` is necessary. - -## Shock Route Tester - -

- Example app screenshot -

- -The Shock Route Tester example app lets you try out the different route types. -Edit the `MyRoutes.swift` file to add your own and test them in the app. - -## License +To record API calls during the UITests execution: -Shock is available under Apache License 2.0. See the LICENSE file for more info. +- Uncomment the line ```app.launchArguments.append("SAVE_API_RESPONSES_ON_DISK")``` +- Make sure that UI Test are not executed in parallel. +- Run the UI Tests (They should fail as the API response changes every time) +- Fix the UI Tests with the new API responses, if you want to adapt them the new responses. +- Copy the new API responses from `data_responses/UITests` folder to `Demo/UITests/Resources/RecordedMocks` folder once you want to update the old API responses. diff --git a/Shock.podspec b/Shock.podspec deleted file mode 100644 index e75030f..0000000 --- a/Shock.podspec +++ /dev/null @@ -1,31 +0,0 @@ -# -# Be sure to run `pod lib lint Shock.podspec' to ensure this is a -# valid spec before submitting. -# -# Any lines starting with a # are optional, but their use is encouraged -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# - -Pod::Spec.new do |s| - s.name = 'Shock' - s.version = ENV['LIB_VERSION'] - s.summary = 'A HTTP mocking framework written in Swift.' - - s.description = <<-DESC -Shock lets you quickly and painlessly provided mock responses for HTTP & HTTPs web requests made by your iOS app. - DESC - - s.homepage = 'https://github.com/justeat/Shock' - s.license = { :type => 'Apache 2.0', :file => 'LICENSE' } - s.author = 'Just Eat Takeaway iOS Team' - s.source = { :git => 'https://github.com/justeat/Shock.git', :tag => s.version.to_s } - s.social_media_url = 'https://twitter.com/justeat_tech' - - s.ios.deployment_target = '10.0' - s.swift_version = '5.0' - - s.source_files = 'Framework/Sources/**/*' - - s.dependency 'SwiftNIOHTTP1', '~> 2.40.0' - s.dependency 'GRMustache.swift', '~> 4.0.1' -end diff --git a/Tests/Sources/ApiCallRequestDataTests.swift b/Tests/Sources/ApiCallRequestDataTests.swift index 9247acb..caba207 100644 --- a/Tests/Sources/ApiCallRequestDataTests.swift +++ b/Tests/Sources/ApiCallRequestDataTests.swift @@ -1,4 +1,4 @@ -// ApiCallRequestData.swift +// ApiCallRequestDataTests.swift import XCTest @testable import Shock @@ -25,7 +25,7 @@ class ApiCallRequestDataTests: XCTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/simple") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/simple") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testSimpleRoute test fixture\n") expectation.fulfill() diff --git a/Tests/Sources/Base/HTTPClient.swift b/Tests/Sources/Base/HTTPClient.swift index e6ca987..54f9b79 100644 --- a/Tests/Sources/Base/HTTPClient.swift +++ b/Tests/Sources/Base/HTTPClient.swift @@ -3,7 +3,7 @@ import Foundation import XCTest -fileprivate let session = URLSession.shared +private let session = URLSession.shared typealias HTTPClientResult = (_ code: Int, _ response: String, _ headers: [String: String], _ error: Error?) -> Void @@ -43,5 +43,4 @@ class HTTPClient { } task.resume() } - } diff --git a/Tests/Sources/Base/ShockTestCase.swift b/Tests/Sources/Base/ShockTestCase.swift index c47196e..9a8c9b8 100644 --- a/Tests/Sources/Base/ShockTestCase.swift +++ b/Tests/Sources/Base/ShockTestCase.swift @@ -19,5 +19,4 @@ class ShockTestCase: XCTestCase { server.stop() super.tearDown() } - } diff --git a/Tests/Sources/ClosureMiddlewareTests.swift b/Tests/Sources/ClosureMiddlewareTests.swift index 00012dd..73753a9 100644 --- a/Tests/Sources/ClosureMiddlewareTests.swift +++ b/Tests/Sources/ClosureMiddlewareTests.swift @@ -11,7 +11,7 @@ class ClosureMiddlewareTests: ShockTestCase { let expectedHeaders = [ "X-Test-Header": "Test" ] let expectedStatusCode = 200 - let middleware = ClosureMiddleware { request, response, next in + let middleware = ClosureMiddleware { _, response, next in response.statusCode = expectedStatusCode response.headers = expectedHeaders response.responseBody = expectedResponseBody.data(using: .utf8) diff --git a/Tests/Sources/CustomRouteTests.swift b/Tests/Sources/CustomRouteTests.swift index 316fee3..2c3b85e 100644 --- a/Tests/Sources/CustomRouteTests.swift +++ b/Tests/Sources/CustomRouteTests.swift @@ -17,7 +17,7 @@ class CustomRouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/custom") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/custom") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testCustomRoute test fixture\n") expectation.fulfill() @@ -40,7 +40,7 @@ class CustomRouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/custom-with-header", headers: customHeaders) { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/custom-with-header", headers: customHeaders) { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testCustomRoute test fixture\n") expectation.fulfill() @@ -49,7 +49,7 @@ class CustomRouteTests: ShockTestCase { } func testCustomRouteWithoutRequestHeader() { - let customHeaders = ["My-Custom-Header" : "my-header-value"] + let customHeaders = ["My-Custom-Header": "my-header-value"] let route: MockHTTPRoute = .custom( method: .get, urlPath: "/custom-with-header", @@ -63,7 +63,7 @@ class CustomRouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 404 response with empty response body") - HTTPClient.get(url: "\(server.hostURL)/custom-with-header") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/custom-with-header") { code, body, _, _ in expectation.fulfill() XCTAssertEqual(code, 404) XCTAssertEqual(body, "") @@ -86,7 +86,7 @@ class CustomRouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/custom-with-header") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/custom-with-header") { code, body, headers, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testCustomRoute test fixture\n") for (k, v) in customHeaders { @@ -122,13 +122,13 @@ class CustomRouteTests: ShockTestCase { server.setup(route: routes) let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/custom-with-query?\(query)") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/custom-with-query?\(query)") { code, body, _, _ in expectation.fulfill() XCTAssertEqual(code, 200) XCTAssertEqual(body, "testCustomRoute test fixture\n") } let expectation2 = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/custom-with-query?\(query2)") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/custom-with-query?\(query2)") { code, body, _, _ in expectation2.fulfill() XCTAssertEqual(code, 200) XCTAssertEqual(body, "testCustomRoute2 test fixture\n") @@ -161,13 +161,13 @@ class CustomRouteTests: ShockTestCase { server.setup(route: routes) let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/custom-with-query?\(query)") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/custom-with-query?\(query)") { code, body, _, _ in expectation.fulfill() XCTAssertEqual(code, 200) XCTAssertEqual(body, "testCustomRoute test fixture\n") } let expectation2 = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/custom-with-query?\(query2)") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/custom-with-query?\(query2)") { code, body, _, _ in expectation2.fulfill() XCTAssertEqual(code, 200) XCTAssertEqual(body, "testCustomRoute2 test fixture\n") @@ -189,7 +189,7 @@ class CustomRouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 404 response with empty response body") - HTTPClient.get(url: "\(server.hostURL)/custom-with-query") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/custom-with-query") { code, body, _, _ in expectation.fulfill() XCTAssertEqual(code, 404) XCTAssertEqual(body, "") @@ -211,7 +211,7 @@ class CustomRouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with no response body") - HTTPClient.get(url: "\(server.hostURL)/custom-with-query") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/custom-with-query") { code, body, _, _ in expectation.fulfill() XCTAssertEqual(code, 200) XCTAssertEqual(body, "") diff --git a/Tests/Sources/MethodTests.swift b/Tests/Sources/MethodTests.swift index 2a5c371..af0136d 100644 --- a/Tests/Sources/MethodTests.swift +++ b/Tests/Sources/MethodTests.swift @@ -14,7 +14,7 @@ class MethodTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/simple") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/simple") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testSimpleRoute test fixture\n") expectation.fulfill() @@ -31,7 +31,7 @@ class MethodTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.post(url: "\(server.hostURL)/simple") { code, body, headers, error in + HTTPClient.post(url: "\(server.hostURL)/simple") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testSimpleRoute test fixture\n") expectation.fulfill() @@ -48,7 +48,7 @@ class MethodTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.post(url: "\(server.hostURL)/simple") { code, body, headers, error in + HTTPClient.post(url: "\(server.hostURL)/simple") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "") expectation.fulfill() @@ -71,10 +71,10 @@ class MethodTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.post(url: "\(server.hostURL)/auth") { code, body, headers, error in + HTTPClient.post(url: "\(server.hostURL)/auth") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testSimpleRoute test fixture\n") - HTTPClient.post(url: "\(self.server.hostURL)/simple") { code, body, headers, error in + HTTPClient.post(url: "\(self.server.hostURL)/simple") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "") expectation.fulfill() @@ -92,7 +92,7 @@ class MethodTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.put(url: "\(server.hostURL)/simple") { code, body, headers, error in + HTTPClient.put(url: "\(server.hostURL)/simple") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testSimpleRoute test fixture\n") expectation.fulfill() @@ -109,7 +109,7 @@ class MethodTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.delete(url: "\(server.hostURL)/simple") { code, body, headers, error in + HTTPClient.delete(url: "\(server.hostURL)/simple") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testSimpleRoute test fixture\n") expectation.fulfill() diff --git a/Tests/Sources/MockHTTPMethodTests.swift b/Tests/Sources/MockHTTPMethodTests.swift index 63b4fe1..01df26c 100644 --- a/Tests/Sources/MockHTTPMethodTests.swift +++ b/Tests/Sources/MockHTTPMethodTests.swift @@ -88,8 +88,8 @@ class MockHTTPMethodTests: XCTestCase { encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] let route: MockHTTPRoute = .custom(method: .get, urlPath: "/my/api/endpoint", - query: ["queryKey" : "custom-header-value"], - requestHeaders: ["X-Custom-Header" : "custom-header-value"], + query: ["queryKey": "custom-header-value"], + requestHeaders: ["X-Custom-Header": "custom-header-value"], responseHeaders: ["Content-Type": "application/json"], code: 200, filename: "my-test-data.json") diff --git a/Tests/Sources/ParallelServerTests.swift b/Tests/Sources/ParallelServerTests.swift index afdc1f2..cd1fca7 100644 --- a/Tests/Sources/ParallelServerTests.swift +++ b/Tests/Sources/ParallelServerTests.swift @@ -10,7 +10,7 @@ class ParallelServerTests: XCTestCase { var servers: [MockServer] = [] - range.forEach { port in + range.forEach { _ in let server = MockServer(portRange: range, bundle: Bundle.module) server.start() servers.append(server) diff --git a/Tests/Sources/RouteTests.swift b/Tests/Sources/RouteTests.swift index f8561ac..3cc686e 100644 --- a/Tests/Sources/RouteTests.swift +++ b/Tests/Sources/RouteTests.swift @@ -11,7 +11,7 @@ class RouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/simple") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/simple") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testSimpleRoute test fixture\n") expectation.fulfill() @@ -25,7 +25,7 @@ class RouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/simple/1") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/simple/1") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testSimpleRoute (with variables) test fixture\n") expectation.fulfill() @@ -40,7 +40,7 @@ class RouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/simple/withvariables/1") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/simple/withvariables/1") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testSimpleRoute (with variables) test fixture\n") expectation.fulfill() @@ -55,7 +55,7 @@ class RouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/simple/withvariables/1") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/simple/withvariables/1") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testSimpleRoute (with variables) test fixture\n") expectation.fulfill() @@ -70,7 +70,7 @@ class RouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body") - HTTPClient.get(url: "\(server.hostURL)/simple/withoutvariables") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/simple/withoutvariables") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testSimpleRoute test fixture\n") expectation.fulfill() @@ -87,7 +87,7 @@ class RouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with response body after redirect") - HTTPClient.get(url: "\(server.hostURL)/redirect") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/redirect") { code, body, _, _ in XCTAssertEqual(code, 200) XCTAssertEqual(body, "testRedirectRoute test fixture\n") expectation.fulfill() @@ -107,7 +107,7 @@ class RouteTests: ShockTestCase { let expectation = self.expectation(description: "This expectation should NOT be fulfilled") - HTTPClient.get(url: "\(server.hostURL)/timeouttest", timeout: 2) { _,_,_,error in + HTTPClient.get(url: "\(server.hostURL)/timeouttest", timeout: 2) { _, _, _, error in XCTAssertNotNil(error, "Request should have errored") expectation.fulfill() } @@ -148,53 +148,53 @@ class RouteTests: ShockTestCase { } func testCustomRouteEquivalence() { - var route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) - var route2 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) + var route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query": "value"], + requestHeaders: ["HTTPHeader": "false"], responseHeaders: ["HTTPHeader": "true"], code: 200, filename: nil) + var route2 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query": "value"], + requestHeaders: ["HTTPHeader": "false"], responseHeaders: ["HTTPHeader": "true"], code: 200, filename: nil) XCTAssertEqual(route1, route2, "Custom routes should be equal") - route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) - route2 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 404, filename: nil) + route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query": "value"], + requestHeaders: ["HTTPHeader": "false"], responseHeaders: ["HTTPHeader": "true"], code: 200, filename: nil) + route2 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query": "value"], + requestHeaders: ["HTTPHeader": "false"], responseHeaders: ["HTTPHeader": "true"], code: 404, filename: nil) XCTAssertEqual(route1, route2, "Codes are different, should not affect equality") - route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) - route2 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query":"value2"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) + route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query": "value"], + requestHeaders: ["HTTPHeader": "false"], responseHeaders: ["HTTPHeader": "true"], code: 200, filename: nil) + route2 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query": "value2"], + requestHeaders: ["HTTPHeader": "false"], responseHeaders: ["HTTPHeader": "true"], code: 200, filename: nil) XCTAssertNotEqual(route1, route2, "Queries are different, should not be equal") - route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) - route2 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"true"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) + route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query": "value"], + requestHeaders: ["HTTPHeader": "false"], responseHeaders: ["HTTPHeader": "true"], code: 200, filename: nil) + route2 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query": "value"], + requestHeaders: ["HTTPHeader": "true"], responseHeaders: ["HTTPHeader": "true"], code: 200, filename: nil) XCTAssertNotEqual(route1, route2, "Request headers are different, should not be equal") - route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) - route2 = MockHTTPRoute.custom(method: .get, urlPath: "bar/foo", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) + route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query": "value"], + requestHeaders: ["HTTPHeader": "false"], responseHeaders: ["HTTPHeader": "true"], code: 200, filename: nil) + route2 = MockHTTPRoute.custom(method: .get, urlPath: "bar/foo", query: ["query": "value"], + requestHeaders: ["HTTPHeader": "false"], responseHeaders: ["HTTPHeader": "true"], code: 200, filename: nil) XCTAssertNotEqual(route1, route2, "Paths are different, should not be equal") - route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) - route2 = MockHTTPRoute.custom(method: .post, urlPath: "foo/bar", query: ["query":"value"], - requestHeaders: ["HTTPHeader":"false"], responseHeaders: ["HTTPHeader":"true"], code: 200, filename: nil) + route1 = MockHTTPRoute.custom(method: .get, urlPath: "foo/bar", query: ["query": "value"], + requestHeaders: ["HTTPHeader": "false"], responseHeaders: ["HTTPHeader": "true"], code: 200, filename: nil) + route2 = MockHTTPRoute.custom(method: .post, urlPath: "foo/bar", query: ["query": "value"], + requestHeaders: ["HTTPHeader": "false"], responseHeaders: ["HTTPHeader": "true"], code: 200, filename: nil) XCTAssertNotEqual(route1, route2, "Methods are different, should not be equal") } func testTemplateRouteEquivalence() { - var route1 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value" : 1]) - var route2 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value" : 1]) + var route1 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value": 1]) + var route2 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value": 1]) XCTAssertEqual(route1, route2, "Template routes should be equal") - route1 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value" : 1]) - route2 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value" : 2]) + route1 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value": 1]) + route2 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value": 2]) XCTAssertEqual(route1, route2, "Templates are different, should not affect equality") - route1 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value" : 1]) - route2 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 404, filename: nil, templateInfo: ["Value" : 1]) + route1 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value": 1]) + route2 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 404, filename: nil, templateInfo: ["Value": 1]) XCTAssertEqual(route1, route2, "Codes are different, should not affect equality") - route1 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value" : 1]) - route2 = MockHTTPRoute.template(method: .get, urlPath: "bar/foo", code: 200, filename: nil, templateInfo: ["Value" : 1]) + route1 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value": 1]) + route2 = MockHTTPRoute.template(method: .get, urlPath: "bar/foo", code: 200, filename: nil, templateInfo: ["Value": 1]) XCTAssertNotEqual(route1, route2, "Paths are different, should not be equal") - route1 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value" : 1]) - route2 = MockHTTPRoute.template(method: .post, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value" : 1]) + route1 = MockHTTPRoute.template(method: .get, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value": 1]) + route2 = MockHTTPRoute.template(method: .post, urlPath: "foo/bar", code: 200, filename: nil, templateInfo: ["Value": 1]) XCTAssertNotEqual(route1, route2, "Methods are different, should not be equal") } diff --git a/Tests/Sources/SocketServerTests.swift b/Tests/Sources/SocketServerTests.swift deleted file mode 100644 index a46feb6..0000000 --- a/Tests/Sources/SocketServerTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -// SocketServerTests.swift - -import XCTest -import JustLog -import Shock - -class SocketServerTests: ShockTestCase { - - func testFakeLogstash() { - let expectation = self.expectation(description: "Expect log echo'd back") - - let route = MockSocketRoute.logStashEcho { (log) in - expectation.fulfill() - } - server.setupSocket(route: route) - server.start() - - let logger = Logger.shared - logger.logstashHost = "localhost" - logger.logstashPort = UInt16(server.selectedSocketPort) - logger.enableLogstashLogging = true - logger.logLogstashSocketActivity = true - logger.logstashTimeout = 1.0 - logger.allowUntrustedServer = true - logger.setup() - - logger.info("Hello world!") - - self.waitForExpectations(timeout: 6.0, handler: nil) - } -} diff --git a/Tests/Sources/TemplatedRouteTests.swift b/Tests/Sources/TemplatedRouteTests.swift index 91c95d0..1379382 100644 --- a/Tests/Sources/TemplatedRouteTests.swift +++ b/Tests/Sources/TemplatedRouteTests.swift @@ -18,7 +18,7 @@ class TemplatedRouteTests: ShockTestCase { let expectation = self.expectation(description: "Expect 200 response with valid generated response body") - HTTPClient.get(url: "\(server.hostURL)/template") { code, body, headers, error in + HTTPClient.get(url: "\(server.hostURL)/template") { _, body, _, _ in expectation.fulfill() let data = body.data(using: .utf8)! let dict = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any?] @@ -29,5 +29,4 @@ class TemplatedRouteTests: ShockTestCase { } self.waitForExpectations(timeout: 2.0, handler: nil) } - } diff --git a/assets/example-app.png b/assets/example-app.png deleted file mode 100644 index 80db2fc..0000000 Binary files a/assets/example-app.png and /dev/null differ