From 755d2db063151a848fd1b297a196205e12bb6baa Mon Sep 17 00:00:00 2001 From: Alberto De Bortoli Date: Wed, 18 Oct 2023 16:40:25 +0100 Subject: [PATCH] Bring internal version of component to public repository Co-authored-by: Andrea Scuderi Co-authored-by: Alberto De Bortoli --- .../workflows/publish-to-trunk-workflow.yml | 20 - .github/workflows/pull-request-workflow.yml | 29 +- .gitignore | 106 +++- .ruby-version | 1 - .../xcshareddata/xcschemes/Shock.xcscheme | 44 +- Demo/BuildConfigurations/DemoApp.xcconfig | 21 + Demo/BuildConfigurations/Project.xcconfig | 55 ++ Demo/BuildConfigurations/UITests.xcconfig | 9 + Demo/DemoApp.xcodeproj/project.pbxproj | 595 ++++++++++-------- .../xcshareddata/swiftpm/Package.resolved | 28 +- .../xcshareddata/xcschemes/DemoApp.xcscheme | 99 +++ .../xcshareddata/xcschemes/UITests.xcscheme | 82 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/swiftpm/Package.resolved | 41 ++ Demo/{ => DemoApp}/Info.plist | 0 .../Resources/Base.lproj/LaunchScreen.xib | 0 .../Resources/Base.lproj/Main.storyboard | 243 +++++++ .../Resources/Data/custom-route.json | 0 .../Resources/Data/simple-route.json | 0 .../Data/template-route.json.mustache | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 Demo/{ => DemoApp}/Sources/AppDelegate.swift | 0 Demo/DemoApp/Sources/DogAPI.swift | 32 + Demo/DemoApp/Sources/MockRoutesManager.swift | 113 ++++ Demo/{ => DemoApp}/Sources/MyRoutes.swift | 4 +- .../Sources/RecorderViewController.swift | 90 +++ .../{ => DemoApp}/Sources/SceneDelegate.swift | 2 +- Demo/DemoApp/Sources/ShockRecorder.swift | 86 +++ Demo/DemoApp/Sources/UITestOutputConfig.swift | 31 + .../Sources/ViewController.swift | 9 +- Demo/Resources/Base.lproj/Main.storyboard | 119 ---- Demo/Sources/Info.plist | 25 - Demo/UITests/Info.plist | 22 + .../320d4092848f48cbfd0df5fd94b4af22.json | 12 + ...46-21_001_GET_api_breeds_image_random.json | 1 + Demo/UITests/Sources/DemoAppUITests.swift | 79 +++ Demo/UITests/Sources/UITestOutputConfig.swift | 31 + Demo/UITests/UITests.xctestplan | 33 + .../Extensions/String+PathMatching.swift | 2 +- .../Middleware/ClosureMiddleware.swift | 1 - Framework/Sources/Middleware/Middleware.swift | 14 +- .../Middleware/MockRoutesMiddleware.swift | 4 +- Framework/Sources/MockHTTPMethod.swift | 12 +- .../Sources/MockHTTPResponseFactory.swift | 5 +- Framework/Sources/MockHTTPRoute+Codable.swift | 2 +- Framework/Sources/MockHTTPRoute.swift | 13 +- .../Sources/MockHTTPServerProtocols.swift | 3 +- Framework/Sources/MockServer.swift | 41 +- Framework/Sources/NIO/MockNIOBaseServer.swift | 4 +- .../Sources/NIO/MockNIOHTTPHandler.swift | 8 +- Framework/Sources/NIO/MockNIOHTTPServer.swift | 18 +- .../Sources/NIO/MockNIOSocketHandler.swift | 16 +- .../Sources/NIO/MockNIOSocketServer.swift | 8 +- Gemfile | 5 - Gemfile.lock | 100 --- Package.resolved | 78 ++- Package.swift | 31 +- README.md | 393 +----------- Shock.podspec | 31 - Tests/Sources/ApiCallRequestDataTests.swift | 4 +- Tests/Sources/Base/HTTPClient.swift | 3 +- Tests/Sources/Base/ShockTestCase.swift | 1 - Tests/Sources/ClosureMiddlewareTests.swift | 2 +- Tests/Sources/CustomRouteTests.swift | 22 +- Tests/Sources/MethodTests.swift | 14 +- Tests/Sources/MockHTTPMethodTests.swift | 4 +- Tests/Sources/ParallelServerTests.swift | 2 +- Tests/Sources/RouteTests.swift | 82 +-- Tests/Sources/SocketServerTests.swift | 31 - Tests/Sources/TemplatedRouteTests.swift | 3 +- assets/example-app.png | Bin 92438 -> 0 bytes 74 files changed, 1780 insertions(+), 1254 deletions(-) delete mode 100644 .github/workflows/publish-to-trunk-workflow.yml delete mode 100644 .ruby-version create mode 100644 Demo/BuildConfigurations/DemoApp.xcconfig create mode 100644 Demo/BuildConfigurations/Project.xcconfig create mode 100644 Demo/BuildConfigurations/UITests.xcconfig create mode 100644 Demo/DemoApp.xcodeproj/xcshareddata/xcschemes/DemoApp.xcscheme create mode 100644 Demo/DemoApp.xcodeproj/xcshareddata/xcschemes/UITests.xcscheme create mode 100644 Demo/DemoApp.xcworkspace/contents.xcworkspacedata rename Demo/{DemoApp.xcodeproj/project.xcworkspace => DemoApp.xcworkspace}/xcshareddata/IDEWorkspaceChecks.plist (100%) create mode 100644 Demo/DemoApp.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 Demo/DemoApp.xcworkspace/xcshareddata/swiftpm/Package.resolved rename Demo/{ => DemoApp}/Info.plist (100%) rename Demo/{ => DemoApp}/Resources/Base.lproj/LaunchScreen.xib (100%) create mode 100644 Demo/DemoApp/Resources/Base.lproj/Main.storyboard rename Demo/{ => DemoApp}/Resources/Data/custom-route.json (100%) rename Demo/{ => DemoApp}/Resources/Data/simple-route.json (100%) rename Demo/{ => DemoApp}/Resources/Data/template-route.json.mustache (100%) rename Demo/{ => DemoApp}/Resources/Images.xcassets/AccentColor.colorset/Contents.json (100%) rename Demo/{ => DemoApp}/Resources/Images.xcassets/AppIcon.appiconset/Contents.json (100%) rename Demo/{ => DemoApp}/Sources/AppDelegate.swift (100%) create mode 100644 Demo/DemoApp/Sources/DogAPI.swift create mode 100644 Demo/DemoApp/Sources/MockRoutesManager.swift rename Demo/{ => DemoApp}/Sources/MyRoutes.swift (98%) create mode 100644 Demo/DemoApp/Sources/RecorderViewController.swift rename Demo/{ => DemoApp}/Sources/SceneDelegate.swift (97%) create mode 100644 Demo/DemoApp/Sources/ShockRecorder.swift create mode 100644 Demo/DemoApp/Sources/UITestOutputConfig.swift rename Demo/{ => DemoApp}/Sources/ViewController.swift (94%) delete mode 100644 Demo/Resources/Base.lproj/Main.storyboard delete mode 100644 Demo/Sources/Info.plist create mode 100644 Demo/UITests/Info.plist create mode 100644 Demo/UITests/Resources/RecordedMocks/DemoAppUITests/320d4092848f48cbfd0df5fd94b4af22.json create mode 100644 Demo/UITests/Resources/RecordedMocks/DemoAppUITests/320d4092848f48cbfd0df5fd94b4af22/2023-09-15-14-46-21_001_GET_api_breeds_image_random.json create mode 100644 Demo/UITests/Sources/DemoAppUITests.swift create mode 100644 Demo/UITests/Sources/UITestOutputConfig.swift create mode 100644 Demo/UITests/UITests.xctestplan delete mode 100644 Gemfile delete mode 100644 Gemfile.lock delete mode 100644 Shock.podspec delete mode 100644 Tests/Sources/SocketServerTests.swift delete mode 100644 assets/example-app.png 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 80db2fc2f0083aae96ddfcbc81811e2943360028..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92438 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