diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 51bba2cac74..cd918eadeeb 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -11,7 +11,6 @@ _Optional: add details about technical approach, solutions etc._
_Optional: reference dependencies to other pull requests etc._
-
### Testing
_Describe how to test._
diff --git a/.github/workflows/_reusable_app_release.yml b/.github/workflows/_reusable_app_release.yml
index 1794715c159..266c29bdfe7 100644
--- a/.github/workflows/_reusable_app_release.yml
+++ b/.github/workflows/_reusable_app_release.yml
@@ -7,6 +7,8 @@ on:
is_cloud_build:
type: boolean
default: true
+ countly_enabled:
+ type: boolean
datadog_enabled:
type: boolean
default: false
@@ -91,7 +93,7 @@ jobs:
WIRE_INTERNAL_GITHUB_USER: ${{ secrets.WIRE_INTERNAL_GITHUB_USER }}
WIRE_INTERNAL_GITHUB_TOKEN: ${{ secrets.WIRE_INTERNAL_GITHUB_TOKEN }}
APPCENTER_API_TOKEN: ${{ secrets.APPCENTER_API_TOKEN }}
- APPCENTER_OWNER_NAME: ${{ secrets.APPCENTER_OWNER_NAME }}
+ APPCENTER_OWNER_NAME: "Wire"
S3_BUCKET: "z-lohika"
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@@ -115,6 +117,7 @@ jobs:
PLAYGROUND_TESTFLIGHT_LINK: ${{ secrets.PLAYGROUND_TESTFLIGHT_LINK }}
COUNTLY_PRODUCTION_KEY: ${{ secrets.COUNTLY_PRODUCTION_KEY }}
COUNTLY_INTERNAL_KEY: ${{ secrets.COUNTLY_INTERNAL_KEY }}
+ ENABLE_COUNTLY: ${{ inputs.countly_enabled }}
SLACK_WEBHOOK_URL: ${{ secrets.WIRE_IOS_CI_WEBHOOK }}
SKIP_SECURITY_TESTS: ${{ inputs.skip_security_tests }}
SEND_TO_EXTERNALS: ${{ inputs.distribute_externals }}
@@ -159,7 +162,7 @@ jobs:
uses: actions/cache@v3
with:
path: ${{ env.PACKAGES_DIR }}
- key: ${{ runner.os }}-swiftpm-project-${{ hashFiles('**/*.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ env.ENABLE_DATADOG }}
+ key: ${{ runner.os }}-swiftpm-project-${{ hashFiles('**/*.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ env.ENABLE_COUNTLY }}-${{ env.ENABLE_DATADOG }}
# Resolve Swift Package Dependencies
- name: Resolve Swift Package Dependencies
@@ -186,7 +189,7 @@ jobs:
uses: actions/upload-artifact@v4
if: failure()
with:
- name: build-logs
+ name: build-logs (${{ github.run_id }} - ${{ github.run_attempt}})
path: |
/Users/runner/Library/Developer/Xcode/DerivedData/**/Logs/**
~/Library/Logs/DiagnosticReports/**
@@ -196,7 +199,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v4
with:
- name: post-build-env-${{ inputs.fastlane_action }}
+ name: post-build-env-${{ inputs.fastlane_action }} (${{ github.run_id }} - ${{ github.run_attempt}})
path: |
**/.post_build/*.env
- name: Load .env file
@@ -213,7 +216,7 @@ jobs:
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
- text: "**${{ env.APP_NAME }}** (version: ${{ env.BUILD_VERSION }} build: ${{ env.BUILD_NUMBER }}) is ready to test 🚀\n**DATADOG_ENABLED:** ${{ env.ENABLE_DATADOG }}\n**CHANGELOG:** ${{ needs.changelog.outputs.changelog-url }}\n**Tap on iOS device to install:** ${{ env.BUILD_INSTALL_LINK }}\n**AWS S3 Paths:**\n`${{ steps.base64-decoded-S3_PATHS.outputs.decoded }}`\n**Triggered by:** ${{ github.triggering_actor }}\n**Build log:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n"
+ text: "**${{ env.APP_NAME }}** (version: ${{ env.BUILD_VERSION }} build: ${{ env.BUILD_NUMBER }}) is ready to test 🚀\n**COUNTLY_ENABLED:** ${{ env.ENABLE_COUNTLY }}\n**DATADOG_ENABLED:** ${{ env.ENABLE_DATADOG }}\n**CHANGELOG:** ${{ needs.changelog.outputs.changelog-url }}\n**Tap on iOS device to install:** ${{ env.BUILD_INSTALL_LINK }}\n**AWS S3 Paths:**\n`${{ steps.base64-decoded-S3_PATHS.outputs.decoded }}`\n**Triggered by:** ${{ github.triggering_actor }}\n**Build log:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n"
- name: Notify on Wire if failed
if: failure()
uses: 8398a7/action-slack@v3
diff --git a/.github/workflows/_reusable_app_release_without_changelog.yml b/.github/workflows/_reusable_app_release_without_changelog.yml
index 26f2ae88540..ecb9535c8e0 100644
--- a/.github/workflows/_reusable_app_release_without_changelog.yml
+++ b/.github/workflows/_reusable_app_release_without_changelog.yml
@@ -7,6 +7,8 @@ on:
is_cloud_build:
type: boolean
default: true
+ countly_enabled:
+ type: boolean
datadog_enabled:
type: boolean
default: false
@@ -89,7 +91,7 @@ jobs:
WIRE_INTERNAL_GITHUB_USER: ${{ secrets.WIRE_INTERNAL_GITHUB_USER }}
WIRE_INTERNAL_GITHUB_TOKEN: ${{ secrets.WIRE_INTERNAL_GITHUB_TOKEN }}
APPCENTER_API_TOKEN: ${{ secrets.APPCENTER_API_TOKEN }}
- APPCENTER_OWNER_NAME: ${{ secrets.APPCENTER_OWNER_NAME }}
+ APPCENTER_OWNER_NAME: "Wire"
S3_BUCKET: "z-lohika"
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@@ -113,6 +115,7 @@ jobs:
PLAYGROUND_TESTFLIGHT_LINK: ${{ secrets.PLAYGROUND_TESTFLIGHT_LINK }}
COUNTLY_PRODUCTION_KEY: ${{ secrets.COUNTLY_PRODUCTION_KEY }}
COUNTLY_INTERNAL_KEY: ${{ secrets.COUNTLY_INTERNAL_KEY }}
+ ENABLE_COUNTLY: ${{ inputs.countly_enabled }}
SLACK_WEBHOOK_URL: ${{ secrets.WIRE_IOS_CI_WEBHOOK }}
SKIP_SECURITY_TESTS: ${{ inputs.skip_security_tests }}
SEND_TO_EXTERNALS: ${{ inputs.distribute_externals }}
@@ -150,7 +153,7 @@ jobs:
uses: actions/cache@v3
with:
path: ${{ env.PACKAGES_DIR }}
- key: ${{ runner.os }}-swiftpm-project-${{ hashFiles('**/*.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ env.ENABLE_DATADOG }}
+ key: ${{ runner.os }}-swiftpm-project-${{ hashFiles('**/*.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ env.ENABLE_COUNTLY }}-${{ env.ENABLE_DATADOG }}
- name: Resolve Swift Package Dependencies
run: |
( cd $REPO_ROOT && xcodebuild -resolvePackageDependencies -disableAutomaticPackageResolution -clonedSourcePackagesDirPath "$PACKAGES_DIR" )
diff --git a/.github/workflows/_reusable_run_tests.yml b/.github/workflows/_reusable_run_tests.yml
index ffdf87b40a7..a792cb0d15c 100644
--- a/.github/workflows/_reusable_run_tests.yml
+++ b/.github/workflows/_reusable_run_tests.yml
@@ -1,110 +1,19 @@
on:
workflow_call:
inputs:
- wire-ios:
- type: boolean
- default: false
- required: false
- wire-ui:
- type: boolean
- default: false
- required: false
- wire-ios-sync-engine:
- type: boolean
- default: false
- required: false
- wire-ios-data-model:
- type: boolean
- default: false
- required: false
- wire-ios-system:
- type: boolean
- default: false
- required: false
- wire-system:
- type: boolean
- default: false
- required: false
- wire-ios-request-strategy:
- type: boolean
- default: false
- required: false
- wire-api:
- type: boolean
- default: false
- required: false
- wire-analytics:
- type: boolean
- default: false
- required: false
- wire-datadog:
- type: boolean
- default: false
- required: false
- wire-domain-project:
- type: boolean
- default: false
- required: false
- wire-domain:
- type: boolean
- default: false
- required: false
- wire-ios-transport:
- type: boolean
- default: false
- required: false
- wire-ios-share-engine:
- type: boolean
- default: false
- required: false
- wire-ios-cryptobox:
- type: boolean
- default: false
- required: false
- wire-ios-mocktransport:
- type: boolean
- default: false
- required: false
- wire-ios-notification-engine:
- type: boolean
- default: false
- required: false
- wire-ios-protos:
- type: boolean
- default: false
- required: false
- wire-ios-images:
- type: boolean
- default: false
- required: false
- wire-ios-link-preview:
- type: boolean
- default: false
- required: false
- wire-ios-utilities:
- type: boolean
- default: false
- required: false
- wire-utilities:
- type: boolean
- default: false
- required: false
- wire-ios-testing:
- type: boolean
- default: false
- required: false
- wire-foundation:
- type: boolean
- default: false
- required: false
- scripts:
- type: boolean
- default: false
- required: false
+ folders:
+ type: string
all:
type: boolean
default: false
required: false
+ branch:
+ type: string
+ required: true
+ notify_secret:
+ type: string
+ required: true
+
secrets:
ZENKINS_USERNAME:
required: true
@@ -119,17 +28,26 @@ env: # https://docs.fastlane.tools/getting-started/ios/setup/
jobs:
run-tests:
+ name: Run tests
runs-on: ghcr.io/cirruslabs/macos-runner:sonoma
+ outputs:
+ xcresult_files: ${{ steps.find-xcresults.outputs.xcresult_files }}
env:
GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_USERNAME: ${{ secrets.ZENKINS_USERNAME }}
- SLACK_WEBHOOK_URL: ${{ secrets.WIRE_IOS_CI_WEBHOOK }}
+ SLACK_WEBHOOK_URL: ${{ secrets[inputs.notify_secret] }}
steps:
- uses: actions/checkout@v4
with:
+ ref: ${{ inputs.branch }}
lfs: 'true'
+ - name: Sanitize branch name
+ run: |
+ SANITIZED_BRANCH=$(echo "${{ inputs.branch }}" | sed 's/[^a-zA-Z0-9._-]/_/g')
+ echo "SANITIZED_BRANCH=$SANITIZED_BRANCH" >> $GITHUB_ENV
+
- name: Retrieve Xcode version
run: |
echo "XCODE_VERSION=$(cat .xcode-version)" >> $GITHUB_OUTPUT
@@ -151,13 +69,6 @@ jobs:
path: Carthage
key: ${{ runner.os }}-xcode${{ steps.xcode-version.outputs.XCODE_VERSION }}-carthage-${{ hashFiles('Cartfile.resolved') }}
- - name: Restore Danger results
- id: restore-danger-results
- uses: actions/cache/restore@v4
- with:
- path: Danger.results
- key: ${{ github.event.before || github.event.pull_request.base.sha }}-Danger.results
-
- name: Bootstrap Carthage if no cache
if: steps.cache-carthage.outputs.cache-hit != 'true'
run: ./scripts/carthage.sh bootstrap --platform ios --use-xcframeworks
@@ -168,7 +79,7 @@ jobs:
uses: actions/cache@v3
with:
path: ${{ env.PACKAGES_DIR }}
- key: ${{ runner.os }}-swiftpm-project-${{ hashFiles('**/*.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
+ key: ${{ runner.os }}-swiftpm-project-${{ hashFiles('**/*.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-false
# Resolve Swift Package Dependencies
- name: Resolve Swift Package Dependencies
@@ -180,6 +91,11 @@ jobs:
with:
bundler-cache: true
+ - name: Display all fastlane lanes
+ if: ${{ contains(inputs.folders, 'fastlane') || inputs.all }}
+ run: |
+ bundle exec fastlane lanes
+
- name: Setup workspace
run: |
./setup.sh
@@ -188,542 +104,53 @@ jobs:
run: |
bundle exec fastlane prepare_for_tests
- # Scripts
-
- - name: Test Scripts
- if: ${{ inputs.scripts || inputs.all }}
- run: |
- echo "Scripts has changes"
- echo "Testing Scripts..."
- xcrun swift test --package-path ./scripts
-
- # WireFoundation
-
- - name: Build WireFoundation
- if: ${{ inputs.wire-foundation || inputs.all }}
+ - name: "Test all selected frameworks"
+ id: set_frameworks
run: |
- echo "WireFoundation has changes"
- echo "Building WireFoundation..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireFoundation-Package -resultBundlePath xcodebuild-wire-foundation.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-foundation.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-foundation || inputs.all }}
- with:
- results: xcodebuild-wire-foundation.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ bundle exec fastlane test_frameworks folders:${{ inputs.folders }} all:${{ inputs.all }}
- - name: Test WireFoundation
- if: ${{ inputs.wire-foundation || inputs.all }}
+ - name: Find all .xcresult files
+ if: ${{ always() && github.event_name == 'pull_request' }}
+ id: find-xcresults
run: |
- echo "Testing WireFoundation..."
- xcodebuild test -workspace wire-ios-mono.xcworkspace -scheme WireFoundation-Package -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-foundation.log | bundle exec xcpretty --report junit --output build/reports/WireFoundation.junit
- exit ${PIPESTATUS[0]}
-
- # WireSystem Project
-
- - name: Build WireSystem Project
- if: ${{ inputs.wire-ios-system || inputs.all }}
+ # Find all .xcresult directories and convert them to a JSON array
+ files=`find artifacts -type d -name "*.xcresult" | jq -R . | jq -s .`
+ echo xcresult_files=${files} >> $GITHUB_OUTPUT
+ - name: Debug Print xcresult files
run: |
- echo "WireSystem Project has changes"
- echo "Building WireSystem Project..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireSystem -resultBundlePath xcodebuild-wire-ios-system.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-system.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-system || inputs.all }}
- with:
- results: xcodebuild-wire-ios-system.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireSystem Project
- if: ${{ inputs.wire-ios-system || inputs.all }}
- run: |
- echo "Testing WireSystem Project..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireSystem -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-system.log | bundle exec xcpretty --report junit --output build/reports/WireSystemProject.junit
- exit ${PIPESTATUS[0]}
-
- # WireTesting Project
-
- - name: Build WireTesting Project
- if: ${{ inputs.wire-ios-testing || inputs.all }}
- run: |
- echo "WireTesting Project has changes"
- echo "Building WireTesting Project..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireTesting -resultBundlePath xcodebuild-wire-ios-testing.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-testing.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-testing || inputs.all }}
- with:
- results: xcodebuild-wire-ios-testing.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireTesting Project
- if: ${{ inputs.wire-ios-testing || inputs.all }}
- run: |
- echo "Testing WireTesting Project..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireTesting -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-testing.log | bundle exec xcpretty --report junit --output build/reports/WireTestingProject.junit
- exit ${PIPESTATUS[0]}
-
- # WireUtilities Project
-
- - name: Build WireUtilities Project
- if: ${{ inputs.wire-ios-utilities || inputs.all }}
- run: |
- echo "WireUtilities Project has changes"
- echo "Building WireUtilities Project..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireUtilities -resultBundlePath xcodebuild-wire-ios-utilities.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-utilities.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-utilities || inputs.all }}
- with:
- results: xcodebuild-wire-ios-utilities.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireUtilities Project
- if: ${{ inputs.wire-ios-utilities || inputs.all }}
- run: |
- echo "Testing WireUtilities Project..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireUtilities -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-utilities.log | bundle exec xcpretty --report junit --output build/reports/WireUtilitiesProject.junit
- exit ${PIPESTATUS[0]}
-
- # WireCryptobox
-
- - name: Build WireCryptobox
- if: ${{ inputs.wire-ios-cryptobox || inputs.all }}
- run: |
- echo "WireCryptobox has changes"
- echo "Building WireCryptobox..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireCryptobox -resultBundlePath xcodebuild-wire-ios-cryptobox.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-cryptobox.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-cryptobox || inputs.all }}
- with:
- results: xcodebuild-wire-ios-cryptobox.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireCryptobox
- if: ${{ inputs.wire-ios-cryptobox || inputs.all }}
- run: |
- echo "Testing WireCryptobox..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireCryptobox -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-cryptobox.log | bundle exec xcpretty --report junit --output build/reports/WireCryptobox.junit
- exit ${PIPESTATUS[0]}
-
- # WireTransport
-
- - name: Build WireTransport
- if: ${{ inputs.wire-ios-transport || inputs.all }}
- run: |
- echo "WireTransport has changes"
- echo "Building WireTransport..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireTransport -resultBundlePath xcodebuild-wire-ios-transport.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-transport.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-transport || inputs.all }}
- with:
- results: xcodebuild-wire-ios-transport.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireTransport
- if: ${{ inputs.wire-ios-transport || inputs.all }}
- run: |
- echo "Testing WireTransport..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireTransport -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-transport.log | bundle exec xcpretty --report junit --output build/reports/WireTransport.junit
- exit ${PIPESTATUS[0]}
-
- # WireLinkPreview
-
- - name: Build WireLinkPreview
- if: ${{ inputs.wire-ios-link-preview || inputs.all }}
- run: |
- echo "WireLinkPreview has changes"
- echo "Building WireLinkPreview..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireLinkPreview -resultBundlePath xcodebuild-wire-ios-link-preview.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-link-preview.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-link-preview || inputs.all }}
- with:
- results: xcodebuild-wire-ios-link-preview.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireLinkPreview
- if: ${{ inputs.wire-ios-link-preview || inputs.all }}
- run: |
- echo "Testing WireLinkPreview..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireLinkPreview -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-link-preview.log | bundle exec xcpretty --report junit --output build/reports/WireLinkPreview.junit
- exit ${PIPESTATUS[0]}
-
- # WireImages
-
- - name: Build WireImages
- if: ${{ inputs.wire-ios-images || inputs.all }}
- run: |
- echo "WireImages has changes"
- echo "Building WireImages..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireImages -resultBundlePath xcodebuild-wire-ios-images.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-images.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-images || inputs.all }}
- with:
- results: xcodebuild-wire-ios-images.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireImages
- if: ${{ inputs.wire-ios-images || inputs.all }}
- run: |
- echo "Testing WireImages..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireImages -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-images.log | bundle exec xcpretty --report junit --output build/reports/WireImages.junit
- exit ${PIPESTATUS[0]}
-
- # WireProtos
-
- - name: Build WireProtos
- if: ${{ inputs.wire-ios-protos || inputs.all }}
- run: |
- echo "WireProtos has changes"
- echo "Building WireProtos..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireProtos -resultBundlePath xcodebuild-wire-ios-protos.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-protos.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-protos || inputs.all }}
- with:
- results: xcodebuild-wire-ios-protos.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireProtos
- if: ${{ inputs.wire-ios-protos || inputs.all }}
- run: |
- echo "Testing WireProtos..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireProtos -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-protos.log | bundle exec xcpretty
- exit ${PIPESTATUS[0]}
-
- # WireMockTransport
-
- - name: Build WireMockTransport
- if: ${{ inputs.wire-ios-mocktransport || inputs.all }}
- run: |
- echo "WireMockTransport has changes"
- echo "Building WireMockTransport..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireMockTransport -resultBundlePath xcodebuild-wire-ios-mocktransport.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-mocktransport.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-mocktransport || inputs.all }}
- with:
- results: xcodebuild-wire-ios-mocktransport.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireMockTransport
- if: ${{ inputs.wire-ios-mocktransport || inputs.all }}
- run: |
- echo "Testing WireMockTransport..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireMockTransport -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-mocktransport.log | bundle exec xcpretty --report junit --output build/reports/WireMockTransport.junit
- exit ${PIPESTATUS[0]}
-
- # WireDataModel
-
- - name: Build WireDataModel
- if: ${{ inputs.wire-ios-data-model || inputs.all }}
- run: |
- echo "WireDataModel has changes"
- echo "Building WireDataModel..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireDataModel -resultBundlePath xcodebuild-wire-ios-data-model.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-data-model.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-data-model || inputs.all }}
- with:
- results: xcodebuild-wire-ios-data-model.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireDataModel
- if: ${{ inputs.wire-ios-data-model || inputs.all }}
- run: |
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireDataModel -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-data-model.log | bundle exec xcpretty --report junit --output build/reports/WireDataModel.junit
- exit ${PIPESTATUS[0]}
-
- # WireAPI
-
- - name: Build WireAPI
- if: ${{ inputs.wire-api || inputs.all }}
- run: |
- echo "WireAPI has changes"
- echo "Building WireAPI..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireAPI -resultBundlePath xcodebuild-wire-api.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-api.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-api || inputs.all }}
- with:
- results: xcodebuild-wire-api.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireAPI
- if: ${{ inputs.wire-api || inputs.all }}
- run: |
- echo "Testing WireAPI..."
- xcodebuild test -workspace wire-ios-mono.xcworkspace -scheme WireAPI -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-api.log | bundle exec xcpretty --report junit --output build/reports/WireAPI.junit
- exit ${PIPESTATUS[0]}
-
- # WireDatadog
-
- - name: Build WireDatadog
- if: ${{ inputs.wire-datadog || inputs.all }}
- run: |
- echo "WireDatadog has changes"
- echo "Building WireDatadog..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireDatadog -resultBundlePath xcodebuild-wire-datadog.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-datadog.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-datadog || inputs.all }}
- with:
- results: xcodebuild-wire-datadog.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- # No tests exist for WireDatadog currently
-
- # WireAnalytics
-
- - name: Build WireAnalytics
- if: ${{ inputs.wire-analytics || inputs.all }}
- run: |
- echo "WireAnalytics has changes"
- echo "Building WireAnalytics..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireAnalytics -resultBundlePath xcodebuild-wire-analytics.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-analytics.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-analytics || inputs.all }}
- with:
- results: xcodebuild-wire-analytics.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- # No tests exist for WireAnalytics currently
-
- # WireDomain
-
- - name: Build WireDomain
- if: ${{ inputs.wire-domain || inputs.all }}
- run: |
- echo "WireDomain has changes"
- echo "Building WireDomain..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireDomainPackage -resultBundlePath xcodebuild-wire-domain.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-domain.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-domain || inputs.all }}
- with:
- results: xcodebuild-wire-domain.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireDomain
- if: ${{ inputs.wire-domain || inputs.all }}
- run: |
- echo "Testing WireDomain..."
- xcodebuild test -workspace wire-ios-mono.xcworkspace -scheme WireDomainPackage -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-domain.log | bundle exec xcpretty --report junit --output build/reports/WireDomain.junit
- exit ${PIPESTATUS[0]}
-
- # WireDomain Project
-
- - name: Build WireDomain Project
- if: ${{ inputs.wire-domain-project || inputs.all }}
- run: |
- echo "WireDomain Project has changes"
- echo "Building WireDomain Project..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireDomain -resultBundlePath xcodebuild-wire-domain-project.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-domain-project.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-domain-project || inputs.all }}
- with:
- results: xcodebuild-wire-domain-project.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireDomain Project
- if: ${{ inputs.wire-domain-project || inputs.all }}
- run: |
- echo "Testing WireDomain Project..."
- xcodebuild test -workspace wire-ios-mono.xcworkspace -scheme WireDomain -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-domain-project.log | bundle exec xcpretty --report junit --output build/reports/WireDomainProject.junit
- exit ${PIPESTATUS[0]}
-
- # WireRequestStrategy
-
- - name: Build WireRequestStrategy
- if: ${{ inputs.wire-ios-request-strategy || inputs.all }}
- run: |
- echo "WireRequestStrategy has changes"
- echo "Building WireRequestStrategy..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireRequestStrategy -resultBundlePath xcodebuild-wire-ios-request-strategy.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-request-strategy.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-request-strategy || inputs.all }}
- with:
- results: xcodebuild-wire-ios-request-strategy.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireRequestStrategy
- if: ${{ inputs.wire-ios-request-strategy || inputs.all }}
- run: |
- echo "Testing WireRequestStrategy..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireRequestStrategy -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-request-strategy.log | bundle exec xcpretty --report junit --output build/reports/WireRequestStrategy.junit
- exit ${PIPESTATUS[0]}
-
- # WireShareEngine
-
- - name: Build WireShareEngine
- if: ${{ inputs.wire-ios-share-engine || inputs.all }}
- run: |
- echo "WireShareEngine has changes"
- echo "Building WireShareEngine..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireShareEngine -resultBundlePath xcodebuild-wire-ios-share-engine.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-share-engine.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-share-engine || inputs.all }}
- with:
- results: xcodebuild-wire-ios-share-engine.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireShareEngine
- if: ${{ inputs.wire-ios-share-engine || inputs.all }}
- run: |
- echo "Testing WireShareEngine..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireShareEngine -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-share-engine.log | bundle exec xcpretty --report junit --output build/reports/WireShareEngine.junit
- exit ${PIPESTATUS[0]}
-
- # WireSyncEngine
-
- - name: Build WireSyncEngine
- if: ${{ inputs.wire-ios-sync-engine || inputs.all }}
- run: |
- echo "WireSyncEngine has changes"
- echo "Building WireSyncEngine..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireSyncEngine -resultBundlePath xcodebuild-wire-ios-sync-engine.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-sync-engine.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-sync-engine || inputs.all }}
- with:
- results: xcodebuild-wire-ios-sync-engine.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireSyncEngine
- if: ${{ inputs.wire-ios-sync-engine || inputs.all }}
- run: |
- echo "Testing WireSyncEngine..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireSyncEngine -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-sync-engine.log | bundle exec xcpretty --report junit --output build/reports/WireSyncEngine.junit
- exit ${PIPESTATUS[0]}
-
- # WireUI
-
- - name: Test WireUI
- if: ${{ inputs.wire-ui || inputs.all }}
- run: |
- echo "WireUI has changes"
- echo "Building WireUI..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireUI-Package -resultBundlePath xcodebuild-wire-ui.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ui.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ui || inputs.all }}
- with:
- results: xcodebuild-wire-ui.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireUI
- if: ${{ inputs.wire-ui || inputs.all }}
- run: |
- echo "Testing WireUI..."
- xcodebuild test -workspace wire-ios-mono.xcworkspace -scheme WireUI-Package -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ui.log | bundle exec xcpretty --report junit --output build/reports/WireUI.junit
- exit ${PIPESTATUS[0]}
-
- # Wire-iOS
-
- - name: Build Wire-iOS
- if: ${{ inputs.wire-ios || inputs.all }}
- run: |
- echo "Wire-iOS has changes"
- echo "Building Wire-iOS..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme Wire-iOS -resultBundlePath xcodebuild-wire-ios.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios || inputs.all }}
+ echo "xcresult files: ${{ steps.find-xcresults.outputs.xcresult_files }}"
+ - name: Upload .xcresult files as artifact
+ if: always()
+ uses: actions/upload-artifact@v4
with:
- results: xcodebuild-wire-ios.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ name: XCResults for ${{ env.SANITIZED_BRANCH }} (${{ github.run_id }} - ${{ github.run_attempt}})
+ path: artifacts/**/*.xcresult # Adjust this if necessary
- - name: Test Wire-iOS
- if: ${{ inputs.wire-ios || inputs.all }}
- run: |
- echo "Testing Wire-iOS..."
- xcodebuild test -workspace wire-ios-mono.xcworkspace -scheme Wire-iOS -testPlan AllTests -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios.log | bundle exec xcpretty --report junit --output build/reports/Wire-iOS-EN.junit
- exit ${PIPESTATUS[0]}
-
- - name: Test Wire-iOS German Locale Tests
- if: ${{ inputs.wire-ios || inputs.all }}
+ # Scripts
+ - name: Test Scripts
+ if: ${{ contains(inputs.folders, 'scripts') || inputs.all }}
run: |
- echo "Testing Wire-iOS German Locale Tests..."
- xcodebuild test -workspace wire-ios-mono.xcworkspace -scheme Wire-iOS -testPlan GermanLocaleTests -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-de.log | bundle exec xcpretty --report junit --output build/reports/Wire-iOS-DE.junit
- exit ${PIPESTATUS[0]}
+ echo "Scripts has changes"
+ echo "Testing Scripts..."
+ xcrun swift test --package-path ./scripts
- name: Upload Failed snapshots
uses: actions/upload-artifact@v4
if: always()
with:
- name: Failed Snapshots and log
+ name: Failed Snapshots and log for ${{ env.SANITIZED_BRANCH }} (${{ github.run_id }} - ${{ github.run_attempt}})
path: |
**/SnapshotResults/
xcodebuild*.log
- # WORKAROUND: if we test WireNotificationEngine and then Wire-iOS, we'll get an error when trying to build
- # Wire-iOS stating that symbols from the notification can't be found. to workaround this, test the notification
- # after the Wire-iOS.
- - name: Build WireNotificationEngine
- if: ${{ inputs.wire-ios-notification-engine || inputs.all }}
- run: |
- echo "WireNotificationEngine has changes"
- echo "Building WireNotificationEngine..."
- xcodebuild build-for-testing -workspace wire-ios-mono.xcworkspace -scheme WireNotificationEngine -resultBundlePath xcodebuild-wire-ios-notification-engine.xcresult -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee xcodebuild-wire-ios-notification-engine.log | bundle exec xcpretty
-
- - uses: wireapp/analyze-xcoderesults-action@v1
- if: ${{ inputs.wire-ios-notification-engine || inputs.all }}
- with:
- results: xcodebuild-wire-ios-notification-engine.xcresult
- warningAnnotations: true
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Test WireNotificationEngine
- if: ${{ inputs.wire-ios-notification-engine || inputs.all }}
- run: |
- echo "Testing WireNotificationEngine..."
- xcodebuild test -retry-tests-on-failure -workspace wire-ios-mono.xcworkspace -scheme WireNotificationEngine -destination 'platform=iOS Simulator,OS=${{ env.IOS_VERSION }},name=${{ env.IPHONE_MODEL }}' | tee -a xcodebuild-wire-ios-notification-engine.log | bundle exec xcpretty --report junit --output build/reports/WireNotificationEngine.junit
- exit ${PIPESTATUS[0]}
-
- - name: Save cache with warning counts
- # This step should only run for the test_develop.yml workflow
- if: ${{ !cancelled() && github.event.action == 'push' }}
- uses: actions/cache/save@v4
- with:
- path: Danger.results
- key: ${{ github.event.head_commit.id || github.event.pull_request.merge_commit_sha || github.event.pull_request.head.sha }}-Danger.results
-
- name: Upload Test Reports as Artifacts
uses: actions/upload-artifact@v4
if: always()
with:
- name: test-reports
+ name: Test reports for ${{ env.SANITIZED_BRANCH }} (${{ github.run_id }} - ${{ github.run_attempt}})
path: |
build/reports/*.junit
+ artifacts/**/*.junit
- name: Prepare visual representation of test results
uses: EnricoMi/publish-unit-test-result-action/macos@v2
@@ -731,34 +158,94 @@ jobs:
with:
files: |
build/reports/*.junit
+ artifacts/**/*.junit
compare_to_earlier_commit: false
- name: Archiving DerivedData Logs
uses: actions/upload-artifact@v4
if: always()
with:
- name: derived-data-xcode
+ name: Derived Data Xcode for ${{env.SANITIZED_BRANCH }} (${{ github.run_id }} - ${{ github.run_attempt}})
path: |
/Users/runner/Library/Developer/Xcode/DerivedData/**/Logs/**
~/Library/Logs/DiagnosticReports/**
- - name: Notify on Wire if failed
- if: ${{ failure() && github.ref_name == 'develop' }}
+ - name: Get Commit SHA
+ if: always()
+ id: get_commit_sha
+ run: echo "commit_sha=$(git rev-parse HEAD)" >> $GITHUB_ENV
+
+ - name: Notify on Wire for iOS Nightly 🤖
+ uses: 8398a7/action-slack@v3
+ if: ${{ inputs.all && always() }}
+ with:
+ status: ${{ job.status }}
+ text: |
+ **Tests for ${{ inputs.branch }}**
+
+ **Commit:** [${{ env.commit_sha }}](https://github.com/wireapp/wire-ios-mono/commit/${{ env.commit_sha }})
+ **Triggered by:** ${{ github.triggering_actor }}
+ **Build log:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+
+ - name: Notify on Wire for iOS CI
uses: 8398a7/action-slack@v3
+ if: ${{ !inputs.all && failure() }}
with:
status: ${{ job.status }}
- text: "🆘 Tests for 'develop' failed 🆘\ncommit: https://github.com/wireapp/wire-ios-mono/commit/${{ github.sha }}\n**Triggered by:** ${{ github.triggering_actor }}\n**Build log:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n"
+ text: |
+ **🚨 Tests for ${{ inputs.branch }} failed 🆘**
+
+ **Commit:** [${{ env.commit_sha }}](https://github.com/wireapp/wire-ios-mono/commit/${{ env.commit_sha }})
+ **Triggered by:** ${{ github.triggering_actor }}
+ **Build log:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+
+ analyze-xcresults:
+ needs: run-tests
+ name: Analyze xcresults
+ if: ${{ always() && github.event_name == 'pull_request' }}
+ runs-on: ghcr.io/cirruslabs/macos-runner:sonoma
+ strategy:
+ matrix:
+ xcresult_file: ${{ fromJson(needs.run-tests.outputs.xcresult_files) }}
+ fail-fast: false # avoid to fail one job
+ steps:
+ - name: Sanitize branch name
+ run: |
+ SANITIZED_BRANCH=$(echo "${{ inputs.branch }}" | sed 's/[^a-zA-Z0-9._-]/_/g')
+ echo "SANITIZED_BRANCH=$SANITIZED_BRANCH" >> $GITHUB_ENV
+
+ - name: Download .xcresult files
+ uses: actions/download-artifact@v4
+ with:
+ name: XCResults for ${{ env.SANITIZED_BRANCH }} (${{ github.run_id }} - ${{ github.run_attempt}})
+ path: ./artifacts
+
+ - name: Display downloaded files
+ run: ls -R
+
+ - name: Analyze .xcresult file
+ uses: wireapp/analyze-xcoderesults-action@v1
+ with:
+ results: ${{ matrix.xcresult_file }}
+ warningAnnotations: true
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
upload-test-results-datadadog:
+ name: Upload to Datadog
runs-on: ubuntu-latest
needs: run-tests
if: always()
steps:
+ - name: Sanitize branch name
+ run: |
+ SANITIZED_BRANCH=$(echo "${{ inputs.branch }}" | sed 's/[^a-zA-Z0-9._-]/_/g')
+ echo "SANITIZED_BRANCH=$SANITIZED_BRANCH" >> $GITHUB_ENV
+
- name: Download tests results
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
continue-on-error: true
with:
- name: test-reports
+ name: Test reports for ${{ env.SANITIZED_BRANCH }} (${{ github.run_id }} - ${{ github.run_attempt}})
- uses: actions/setup-node@v3
with:
node-version: 18
diff --git a/.github/workflows/beta_app_release.yml b/.github/workflows/beta_app_release.yml
index 69ea1131bfc..427d1eefafe 100644
--- a/.github/workflows/beta_app_release.yml
+++ b/.github/workflows/beta_app_release.yml
@@ -14,6 +14,7 @@ jobs:
testflight_beta:
uses: ./.github/workflows/_reusable_app_release.yml
with:
+ countly_enabled: true
datadog_enabled: true
fastlane_action: testflight_beta
distribute_externals: ${{ inputs.distribute_externals }}
diff --git a/.github/workflows/build_pr.yml b/.github/workflows/build_pr.yml
index d960fee4cfc..70cfae282b7 100644
--- a/.github/workflows/build_pr.yml
+++ b/.github/workflows/build_pr.yml
@@ -24,6 +24,7 @@ jobs:
) && github.event.pull_request.draft == false
uses: ./.github/workflows/_reusable_app_release_without_changelog.yml
with:
+ countly_enabled: false
datadog_enabled: false
fastlane_action: development_pr
is_cloud_build: true
diff --git a/.github/workflows/c1_c2_c3_app_release_production.yml b/.github/workflows/c1_c2_c3_app_release_production.yml
index 384fa7b99c7..6e5e6d3ce80 100644
--- a/.github/workflows/c1_c2_c3_app_release_production.yml
+++ b/.github/workflows/c1_c2_c3_app_release_production.yml
@@ -24,6 +24,7 @@ jobs:
with:
fastlane_action: appstore_col_1_prod
is_cloud_build: false
+ countly_enabled: false
skip_security_tests: false
secrets: inherit
@@ -34,6 +35,7 @@ jobs:
with:
fastlane_action: appstore_col_2_prod
is_cloud_build: false
+ countly_enabled: false
skip_security_tests: false
secrets: inherit
@@ -43,5 +45,6 @@ jobs:
with:
fastlane_action: appstore_col_3_prod
is_cloud_build: false
+ countly_enabled: false
skip_security_tests: false
secrets: inherit
diff --git a/.github/workflows/c1_c2_c3_app_release_restricted.yml b/.github/workflows/c1_c2_c3_app_release_restricted.yml
index 25ddaa56b3e..ca724a985a0 100644
--- a/.github/workflows/c1_c2_c3_app_release_restricted.yml
+++ b/.github/workflows/c1_c2_c3_app_release_restricted.yml
@@ -31,6 +31,7 @@ jobs:
with:
fastlane_action: appstore_col_1_restricted
is_cloud_build: false
+ countly_enabled: false
datadog_enabled: ${{ inputs.datadog_enabled }}
skip_security_tests: ${{ inputs.skip_security_tests }}
secrets: inherit
@@ -41,6 +42,7 @@ jobs:
with:
fastlane_action: appstore_col_2_restricted
is_cloud_build: false
+ countly_enabled: false
datadog_enabled: ${{ inputs.datadog_enabled }}
skip_security_tests: ${{ inputs.skip_security_tests }}
secrets: inherit
@@ -51,6 +53,7 @@ jobs:
with:
fastlane_action: appstore_col_3_restricted
is_cloud_build: false
+ countly_enabled: false
datadog_enabled: ${{ inputs.datadog_enabled }}
skip_security_tests: ${{ inputs.skip_security_tests }}
secrets: inherit
diff --git a/.github/workflows/c1_c3_app_release_production.yml b/.github/workflows/c1_c3_app_release_production.yml
index cab902bd6b1..084e1a15a65 100644
--- a/.github/workflows/c1_c3_app_release_production.yml
+++ b/.github/workflows/c1_c3_app_release_production.yml
@@ -11,6 +11,7 @@ jobs:
with:
fastlane_action: appstore_col_1_prod
is_cloud_build: false
+ countly_enabled: false
skip_security_tests: false
secrets: inherit
@@ -19,5 +20,6 @@ jobs:
with:
fastlane_action: appstore_col_3_prod
is_cloud_build: false
+ countly_enabled: false
skip_security_tests: false
secrets: inherit
diff --git a/.github/workflows/c1_c3_app_release_restricted.yml b/.github/workflows/c1_c3_app_release_restricted.yml
index 6859e68a449..98c29ba3da5 100644
--- a/.github/workflows/c1_c3_app_release_restricted.yml
+++ b/.github/workflows/c1_c3_app_release_restricted.yml
@@ -10,6 +10,7 @@ jobs:
uses: ./.github/workflows/_reusable_app_release.yml
with:
fastlane_action: appstore_col_1_restricted
+ countly_enabled: false
skip_security_tests: false
secrets: inherit
@@ -17,5 +18,6 @@ jobs:
uses: ./.github/workflows/_reusable_app_release.yml
with:
fastlane_action: appstore_col_3_restricted
+ countly_enabled: false
skip_security_tests: false
secrets: inherit
diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml
index a12cf155be9..e31ad0203e3 100644
--- a/.github/workflows/development.yml
+++ b/.github/workflows/development.yml
@@ -13,6 +13,7 @@ jobs:
development:
uses: ./.github/workflows/_reusable_app_release.yml
with:
+ countly_enabled: true
datadog_enabled: true
fastlane_action: development
is_cloud_build: true
diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml
index a661d6ecade..c736c0f2bd8 100644
--- a/.github/workflows/playground.yml
+++ b/.github/workflows/playground.yml
@@ -15,6 +15,7 @@ jobs:
playground:
uses: ./.github/workflows/_reusable_app_release.yml
with:
+ countly_enabled: true
datadog_enabled: true
distribute_externals: ${{ inputs.distribute_externals }}
fastlane_action: playground
diff --git a/.github/workflows/test_develop.yml b/.github/workflows/test_develop.yml
index 8dcc66dcc92..bc9ab833c19 100644
--- a/.github/workflows/test_develop.yml
+++ b/.github/workflows/test_develop.yml
@@ -1,19 +1,24 @@
-name: Test Develop
+name: Nightly Tests
on:
workflow_dispatch:
- push:
- branches:
- - 'develop'
-
+ schedule:
+ - cron: "0 21 * * MON-FRI"
+
# This is what will cancel the workflow
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
- trigger_tests_develop:
+ trigger_tests:
+ strategy:
+ matrix:
+ branch: [develop, release/cycle-3.114] # List other branches here
+ fail-fast: false # avoid to fail one job
uses: ./.github/workflows/_reusable_run_tests.yml
with:
all: true
+ branch: ${{ matrix.branch }}
+ notify_secret: WIRE_IOS_NIGHTLY_WEBHOOK
secrets: inherit
diff --git a/.github/workflows/test_pr_changes.yml b/.github/workflows/test_pr_changes.yml
index 477b7413729..3a321e4532b 100644
--- a/.github/workflows/test_pr_changes.yml
+++ b/.github/workflows/test_pr_changes.yml
@@ -15,6 +15,7 @@ jobs:
# This job will tell us which frameworks have source code changes.
# We'll use the results to run tests only for changed frameworks.
detect-changes:
+ name: Detect PR Changes
if: >
github.event.action != 'edited' || (
github.event.changes.title == null &&
@@ -26,42 +27,23 @@ jobs:
runs-on: ubuntu-latest
# Set job outputs to values from filter step
outputs:
- wire-ios: ${{ steps.filter.outputs.wire-ios == 'true' || steps.filter.outputs.carthage == 'true' }}
- wire-ui: ${{ steps.filter.outputs.wire-ui }}
- wire-ios-sync-engine: ${{ steps.filter.outputs.wire-ios-sync-engine }}
- wire-ios-data-model: ${{ steps.filter.outputs.wire-ios-data-model }}
- wire-ios-system: ${{ steps.filter.outputs.wire-ios-system }}
- wire-ios-request-strategy: ${{ steps.filter.outputs.wire-ios-request-strategy }}
- wire-api: ${{ steps.filter.outputs.wire-api }}
- wire-analytics: ${{ steps.filter.outputs.wire-analytics }}
- wire-datadog: ${{ steps.filter.outputs.wire-datadog }}
- wire-domain-project: ${{ steps.filter.outputs.wire-domain-project }}
- wire-domain: ${{ steps.filter.outputs.wire-domain }}
- wire-ios-transport: ${{ steps.filter.outputs.wire-ios-transport }}
- wire-ios-share-engine: ${{ steps.filter.outputs.wire-ios-share-engine }}
- wire-ios-cryptobox: ${{ steps.filter.outputs.wire-ios-cryptobox }}
- wire-ios-mocktransport: ${{ steps.filter.outputs.wire-ios-mocktransport }}
- wire-ios-notification-engine: ${{ steps.filter.outputs.wire-ios-notification-engine }}
- wire-ios-protos: ${{ steps.filter.outputs.wire-ios-protos }}
- wire-ios-images: ${{ steps.filter.outputs.wire-ios-images }}
- wire-ios-link-preview : ${{ steps.filter.outputs.wire-ios-link-preview }}
- wire-ios-utilities: ${{ steps.filter.outputs.wire-ios-utilities }}
- wire-ios-testing: ${{ steps.filter.outputs.wire-ios-testing }}
- wire-foundation: ${{ steps.filter.outputs.wire-foundation }}
- scripts: ${{ steps.filter.outputs.scripts }}
+ folders: ${{ steps.filter.outputs.changes }}
steps:
- uses: actions/checkout@v4
with:
- lfs: 'true'
+ lfs: 'false' # as long as we use lfs only for snapshotTesting it should not be useful here
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
+ fastlane:
+ - 'fastlane/**'
wire-ios:
- 'wire-ios/**'
- wire-ui:
+ WireUI:
- 'WireUI/**'
+ - 'wire-ios-mono.xcworkspace/xcshareddata/xcschemes/WireUIAll.xcscheme'
wire-ios-share-engine:
- 'wire-ios-share-engine/**'
wire-ios-notification-engine:
@@ -70,20 +52,17 @@ jobs:
- 'wire-ios-sync-engine/**'
wire-ios-request-strategy:
- 'wire-ios-request-strategy/**'
- wire-api:
+ WireAPI:
- 'WireAPI/**'
- wire-analytics:
- - 'WireAnalytics/Sources/WireAnalytics/**'
- wire-datadog:
- - 'WireAnalytics/Sources/WireDatadog/**'
- wire-domain-project:
- - 'WireDomain/Project/**'
- - 'WireDomain/Sources/WireDomain/**'
- - 'WireDomain/Sources/WireDomainSupport/**'
- wire-domain:
- - 'WireDomain/*.*'
- - 'WireDomain/Sources/Package/**'
- - 'WireDomain/Sources/PackageSupport/**'
+ - 'wire-ios-mono.xcworkspace/xcshareddata/xcschemes/WireAPIAll.xcscheme'
+ WireAnalytics:
+ - 'WireAnalytics/**'
+ - 'wire-ios-mono.xcworkspace/xcshareddata/xcschemes/WireAnalyticsAll.xcscheme'
+ WireDomain:
+ - 'WireDomain/**'
+ WireLogging:
+ - 'WireLogging/**'
+ - 'wire-ios-mono.xcworkspace/xcshareddata/xcschemes/WireLoggingAll.xcscheme'
wire-ios-data-model:
- 'wire-ios-data-model/**'
wire-ios-transport:
@@ -102,8 +81,9 @@ jobs:
- 'wire-ios-utilities/**'
wire-ios-testing:
- 'wire-ios-testing/**'
- wire-foundation:
+ WireFoundation:
- 'WireFoundation/**'
+ - 'wire-ios-mono.xcworkspace/xcshareddata/xcschemes/WireFoundationAll.xcscheme'
wire-ios-system:
- 'wire-ios-system/**'
scripts:
@@ -113,29 +93,10 @@ jobs:
trigger_tests_pr:
needs: detect-changes
+ name: PR Tests
uses: ./.github/workflows/_reusable_run_tests.yml
with:
- wire-ios: ${{ needs.detect-changes.outputs.wire-ios == 'true' }}
- wire-ui: ${{ needs.detect-changes.outputs.wire-ui == 'true' }}
- wire-ios-sync-engine: ${{ needs.detect-changes.outputs.wire-ios-sync-engine == 'true' }}
- wire-ios-data-model: ${{ needs.detect-changes.outputs.wire-ios-data-model == 'true' }}
- wire-ios-system: ${{ needs.detect-changes.outputs.wire-ios-system == 'true' }}
- wire-ios-request-strategy: ${{ needs.detect-changes.outputs.wire-ios-request-strategy == 'true' }}
- wire-api: ${{ needs.detect-changes.outputs.wire-api == 'true' }}
- wire-analytics: ${{ needs.detect-changes.outputs.wire-analytics == 'true' }}
- wire-datadog: ${{ needs.detect-changes.outputs.wire-datadog == 'true' }}
- wire-domain-project: ${{ needs.detect-changes.outputs.wire-domain-project == 'true' }}
- wire-domain: ${{ needs.detect-changes.outputs.wire-domain == 'true' }}
- wire-ios-transport: ${{ needs.detect-changes.outputs.wire-ios-transport == 'true' }}
- wire-ios-share-engine: ${{ needs.detect-changes.outputs.wire-ios-share-engine == 'true' }}
- wire-ios-cryptobox: ${{ needs.detect-changes.outputs.wire-ios-cryptobox == 'true' }}
- wire-ios-mocktransport: ${{ needs.detect-changes.outputs.wire-ios-mocktransport == 'true' }}
- wire-ios-notification-engine: ${{ needs.detect-changes.outputs.wire-ios-notification-engine == 'true' }}
- wire-ios-protos: ${{ needs.detect-changes.outputs.wire-ios-protos == 'true' }}
- wire-ios-images: ${{ needs.detect-changes.outputs.wire-ios-images == 'true' }}
- wire-ios-link-preview: ${{ needs.detect-changes.outputs.wire-ios-link-preview == 'true' }}
- wire-ios-utilities: ${{ needs.detect-changes.outputs.wire-ios-utilities == 'true' }}
- wire-ios-testing: ${{ needs.detect-changes.outputs.wire-ios-testing == 'true' }}
- wire-foundation: ${{ needs.detect-changes.outputs.wire-foundation == 'true' }}
- scripts: ${{ needs.detect-changes.outputs.scripts == 'true' }}
+ folders: ${{ join(fromJson(needs.detect-changes.outputs.folders), ',') }}
+ branch: ${{ github.head_ref }}
+ notify_secret: WIRE_IOS_CI_WEBHOOK
secrets: inherit
diff --git a/.gitignore b/.gitignore
index 502fca9230f..312e39287c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,6 +50,10 @@ playground.xcworkspace
# .swiftpm
.build/
+# Ignore Package.resolved files in packages and projects, only track the one in the workspace.
+**/Package.resolved
+!wire-ios-mono.xcworkspace/xcshareddata/swiftpm/Package.resolved
+**/*-Package.xcscheme
# CocoaPods
#
@@ -97,3 +101,4 @@ vendor/*
# Failing Snapshot Tests
SnapshotResults
+artifacts/
diff --git a/Cartfile b/Cartfile
index b003441554f..d806c954bc5 100644
--- a/Cartfile
+++ b/Cartfile
@@ -3,5 +3,5 @@ github "wireapp/libsodium" "1.0.14"
github "wireapp/ZipArchive" "v2.4.2"
github "wireapp/libPhoneNumber-iOS" "1.1"
github "wireapp/ocmock" "v3.4.3_Xcode14.3.1"
-github "wireapp/core-crypto" "v1.0.2"
+github "wireapp/core-crypto" "v3.0.0"
binary "wire-avs.json"
diff --git a/Cartfile.private b/Cartfile.private
index ea0e37bc750..41b3630c60f 100644
--- a/Cartfile.private
+++ b/Cartfile.private
@@ -1,2 +1,2 @@
# used by Wire-Proto, this is not compiled but just checked out by Carthage.
-github "wireapp/generic-message-proto" "v1.50.0"
+github "wireapp/generic-message-proto" "v1.51.0"
diff --git a/Cartfile.resolved b/Cartfile.resolved
index 44098383289..1991a9fe420 100644
--- a/Cartfile.resolved
+++ b/Cartfile.resolved
@@ -1,8 +1,8 @@
-binary "wire-avs.json" "9.9.10"
+binary "wire-avs.json" "9.10.16"
github "wireapp/ZipArchive" "v2.4.2"
-github "wireapp/core-crypto" "v1.0.2"
+github "wireapp/core-crypto" "v3.0.0"
github "wireapp/cryptobox-ios" "v1.1.0_xcframework_arm64simulator"
-github "wireapp/generic-message-proto" "v1.50.0"
+github "wireapp/generic-message-proto" "v1.51.0"
github "wireapp/libPhoneNumber-iOS" "1.1"
github "wireapp/libsodium" "1.0.14"
github "wireapp/ocmock" "v3.4.3_Xcode14.3.1"
diff --git a/Gemfile b/Gemfile
index 726017b0f68..533a32d2214 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
-ruby File.read('.ruby-version').strip
+ruby file: ".ruby-version"
gem 'fastlane'
gem 'git'
diff --git a/Gemfile.lock b/Gemfile.lock
index 9cbd966673a..4e3796df9cd 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -10,16 +10,16 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
- aws-partitions (1.1009.0)
- aws-sdk-core (3.213.0)
+ aws-partitions (1.1020.0)
+ aws-sdk-core (3.214.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.95.0)
+ aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
- aws-sdk-s3 (1.171.0)
+ aws-sdk-s3 (1.176.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
@@ -97,7 +97,7 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.3.1)
- fastlane (2.225.0)
+ fastlane (2.226.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -137,7 +137,7 @@ GEM
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
- xcpretty (~> 0.3.0)
+ xcpretty (~> 0.4.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-appcenter (2.1.2)
fastlane-plugin-datadog (0.2.0)
@@ -185,7 +185,7 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
- http-cookie (1.0.7)
+ http-cookie (1.0.8)
domain_name (~> 0.5)
httparty (0.22.0)
csv
@@ -193,11 +193,11 @@ GEM
multi_xml (>= 0.5.2)
httpclient (2.8.3)
jmespath (1.6.2)
- json (2.8.2)
+ json (2.9.0)
jwt (2.9.3)
base64
- kramdown (2.5.0)
- rexml (>= 3.3.6)
+ kramdown (2.5.1)
+ rexml (>= 3.3.9)
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
mini_magick (4.13.2)
@@ -206,7 +206,7 @@ GEM
multi_xml (0.7.1)
bigdecimal (~> 3.1)
multipart-post (2.4.1)
- mutex_m (0.2.0)
+ mutex_m (0.3.0)
nanaimo (0.4.0)
nap (1.1.0)
naturally (2.2.1)
@@ -218,7 +218,7 @@ GEM
optparse (0.6.0)
os (1.1.4)
plist (3.7.1)
- pstore (0.1.3)
+ pstore (0.1.4)
public_suffix (6.0.1)
rake (13.2.1)
rchardet (1.8.0)
@@ -228,7 +228,7 @@ GEM
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.3.9)
- rouge (2.0.7)
+ rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
sawyer (0.9.2)
@@ -265,8 +265,8 @@ GEM
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
- xcpretty (0.3.0)
- rouge (~> 2.0.7)
+ xcpretty (0.4.0)
+ rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
xcresult (0.2.2)
diff --git a/WireAPI/.swiftpm/xcode/xcshareddata/xcschemes/WireAPISupport.xcscheme b/WireAPI/.swiftpm/xcode/xcshareddata/xcschemes/WireAPISupport.xcscheme
deleted file mode 100644
index daf73137996..00000000000
--- a/WireAPI/.swiftpm/xcode/xcshareddata/xcschemes/WireAPISupport.xcscheme
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/WireAPI/Package.swift b/WireAPI/Package.swift
index 7faf64bd428..fca52819371 100644
--- a/WireAPI/Package.swift
+++ b/WireAPI/Package.swift
@@ -49,7 +49,8 @@ let package = Package(
.process("APIs/SelfUserAPI/Resources"),
.process("APIs/UserClientsAPI/Resources"),
.process("Network/PushChannel/Resources"),
- .process("Authentication/Resources")
+ .process("Authentication/Resources"),
+ .process("Backend/Resources")
]
)
]
diff --git a/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/FeatureConfigRepositoryError.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPI.swift
similarity index 80%
rename from WireDomain/Sources/WireDomain/Repositories/FeatureConfig/FeatureConfigRepositoryError.swift
rename to WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPI.swift
index 0ea8390e769..eeb37c4d904 100644
--- a/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/FeatureConfigRepositoryError.swift
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPI.swift
@@ -18,12 +18,11 @@
import Foundation
-/// Errors originating from `FeatureConfigRepository`.
+public protocol AccountsAPI {
-enum FeatureConfigRepositoryError: Error {
-
- /// Unable to fetch feature locally
-
- case failedToFetchFeatureLocally
+ /// Convert the account from individual to team.
+ ///
+ ///
+ func upgradeToTeam(teamName: String) async throws -> UpgradedAccountTeam
}
diff --git a/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIBuilder.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIBuilder.swift
new file mode 100644
index 00000000000..80b569282a7
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIBuilder.swift
@@ -0,0 +1,58 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+/// A builder for `AccountsAPI`.
+
+public struct AccountsAPIBuilder {
+
+ let apiService: any APIServiceProtocol
+
+ /// Create a new builder.
+ ///
+ /// - Parameter apiService: An api service.
+
+ public init(apiService: any APIServiceProtocol) {
+ self.apiService = apiService
+ }
+
+ /// Make a versioned `AccountsAPI`.
+ ///
+ /// - Parameter version: An api version.
+ /// - Returns: A versioned `AccountsAPI`.
+
+ public func makeAPI(for version: APIVersion) -> any AccountsAPI {
+ switch version {
+ case .v0:
+ AccountsAPIV0(apiService: apiService)
+ case .v1:
+ AccountsAPIV1(apiService: apiService)
+ case .v2:
+ AccountsAPIV2(apiService: apiService)
+ case .v3:
+ AccountsAPIV3(apiService: apiService)
+ case .v4:
+ AccountsAPIV4(apiService: apiService)
+ case .v5:
+ AccountsAPIV5(apiService: apiService)
+ case .v6:
+ AccountsAPIV6(apiService: apiService)
+ case .v7:
+ AccountsAPIV7(apiService: apiService)
+ }
+ }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIError.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIError.swift
new file mode 100644
index 00000000000..a810383b5bc
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIError.swift
@@ -0,0 +1,35 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+/// Errors originating from `AccountsAPI`.
+public enum AccountsAPIError: Error {
+ /// An error occurred while encoding the request body.
+ case invalidRequestBody
+
+ /// A request url is invalid.
+ case invalidURL
+
+ /// Unsupported endpoint for API version
+ case unsupportedEndpointForAPIVersion
+
+ /// The user is already in a team
+ case userAlreadyInATeam
+
+ /// The user could not be found
+ case userNotFound
+}
diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/ContactsUI/ContactsViewController+SearchHeaderViewControllerDelegate.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV0.swift
similarity index 64%
rename from wire-ios/Wire-iOS/Sources/UserInterface/ContactsUI/ContactsViewController+SearchHeaderViewControllerDelegate.swift
rename to WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV0.swift
index b9445f6640b..5eb8b4f538c 100644
--- a/wire-ios/Wire-iOS/Sources/UserInterface/ContactsUI/ContactsViewController+SearchHeaderViewControllerDelegate.swift
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV0.swift
@@ -16,16 +16,20 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-extension ContactsViewController: SearchHeaderViewControllerDelegate {
+import Foundation
- func searchHeaderViewController(
- _ searchHeaderViewController: SearchHeaderViewController,
- updatedSearchQuery query: String
- ) {
- dataSource.searchQuery = query
+class AccountsAPIV0: AccountsAPI, VersionedAPI {
+ let apiService: any APIServiceProtocol
+
+ init(apiService: any APIServiceProtocol) {
+ self.apiService = apiService
+ }
+
+ var apiVersion: APIVersion {
+ .v0
}
- func searchHeaderViewControllerDidConfirmAction(_ searchHeaderViewController: SearchHeaderViewController) {
- // No op
+ func upgradeToTeam(teamName: String) async throws -> UpgradedAccountTeam {
+ throw AccountsAPIError.unsupportedEndpointForAPIVersion
}
}
diff --git a/WireAnalytics/Sources/WireDatadog/LogLevel.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV1.swift
similarity index 79%
rename from WireAnalytics/Sources/WireDatadog/LogLevel.swift
rename to WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV1.swift
index 7b4acfabcf1..6411912d905 100644
--- a/WireAnalytics/Sources/WireDatadog/LogLevel.swift
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV1.swift
@@ -16,13 +16,8 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-public extension WireDatadog {
- enum LogLevel: String, Codable {
- case debug
- case info
- case notice
- case warn
- case error
- case critical
+class AccountsAPIV1: AccountsAPIV0 {
+ override var apiVersion: APIVersion {
+ .v1
}
}
diff --git a/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV2.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV2.swift
new file mode 100644
index 00000000000..49cc3d9c6f0
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV2.swift
@@ -0,0 +1,23 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+class AccountsAPIV2: AccountsAPIV1 {
+ override var apiVersion: APIVersion {
+ .v2
+ }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV3.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV3.swift
new file mode 100644
index 00000000000..8fcb079fa88
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV3.swift
@@ -0,0 +1,23 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+class AccountsAPIV3: AccountsAPIV2 {
+ override var apiVersion: APIVersion {
+ .v3
+ }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV4.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV4.swift
new file mode 100644
index 00000000000..ef4b73a1adc
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV4.swift
@@ -0,0 +1,23 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+class AccountsAPIV4: AccountsAPIV3 {
+ override var apiVersion: APIVersion {
+ .v4
+ }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV5.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV5.swift
new file mode 100644
index 00000000000..07ebb79890d
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV5.swift
@@ -0,0 +1,23 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+class AccountsAPIV5: AccountsAPIV4 {
+ override var apiVersion: APIVersion {
+ .v5
+ }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV6.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV6.swift
new file mode 100644
index 00000000000..dbc3b60c022
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV6.swift
@@ -0,0 +1,23 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+class AccountsAPIV6: AccountsAPIV5 {
+ override var apiVersion: APIVersion {
+ .v6
+ }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV7.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV7.swift
new file mode 100644
index 00000000000..502bb71674c
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV7.swift
@@ -0,0 +1,60 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+
+class AccountsAPIV7: AccountsAPIV6 {
+ override var apiVersion: APIVersion {
+ .v7
+ }
+
+ override func upgradeToTeam(teamName: String) async throws -> UpgradedAccountTeam {
+ let components = URLComponents(string: "upgrade-personal-to-team")
+
+ guard let url = components?.url else {
+ assertionFailure("generated an invalid url")
+ throw AccountsAPIError.invalidURL
+ }
+
+ let body = UpgradeToTeamRequestBodyV7(name: teamName)
+
+ let encodedJSON: Data
+ do {
+ encodedJSON = try JSONEncoder().encode(body)
+ } catch {
+ assertionFailure("failed to encode body")
+ throw AccountsAPIError.invalidRequestBody
+ }
+
+ let request = URLRequestBuilder(url: url)
+ .withBody(encodedJSON, contentType: .json)
+ .withMethod(.post)
+ .build()
+
+ let (data, response) = try await apiService.executeRequest(
+ request,
+ requiringAccessToken: true
+ )
+
+ return try ResponseParser()
+ .success(code: .ok, type: UpgradeToTeamResponseV7.self)
+ .failure(code: .forbidden, label: "user-already-in-a-team", error: AccountsAPIError.userAlreadyInATeam)
+ .failure(code: .notFound, label: "not-found", error: AccountsAPIError.userNotFound)
+ .parse(code: response.statusCode, data: data)
+ }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/AccountsAPI/Requests/UpgradeToTeamRequestBodyV7.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/Requests/UpgradeToTeamRequestBodyV7.swift
new file mode 100644
index 00000000000..47316b2d4db
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/Requests/UpgradeToTeamRequestBodyV7.swift
@@ -0,0 +1,38 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+
+struct UpgradeToTeamRequestBodyV7: Codable, Sendable {
+ let currency: String?
+ let icon: String
+ let icon_key: String?
+ let name: String
+
+ init(
+ currency: String? = nil,
+ icon: String = "default",
+ icon_key: String? = nil,
+ name: String
+ ) {
+ self.currency = currency
+ self.icon = icon
+ self.icon_key = icon_key
+ self.name = name
+ }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/AccountsAPI/Responses/UpgradeToTeamResponseV7.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/Responses/UpgradeToTeamResponseV7.swift
new file mode 100644
index 00000000000..4f620dd7a94
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/Responses/UpgradeToTeamResponseV7.swift
@@ -0,0 +1,32 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+
+struct UpgradeToTeamResponseV7: Decodable, ToAPIModelConvertible, Sendable {
+
+ /// The team's ID.
+ public let teamId: UUID
+
+ /// The team's name.
+ public let teamName: String
+
+ func toAPIModel() -> UpgradedAccountTeam {
+ UpgradedAccountTeam(teamId: teamId, teamName: teamName)
+ }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/BackendInfoAPI/BackendInfoAPIImpl.swift b/WireAPI/Sources/WireAPI/APIs/BackendInfoAPI/BackendInfoAPIImpl.swift
index 990c863eaa3..76aa9e8dc6a 100644
--- a/WireAPI/Sources/WireAPI/APIs/BackendInfoAPI/BackendInfoAPIImpl.swift
+++ b/WireAPI/Sources/WireAPI/APIs/BackendInfoAPI/BackendInfoAPIImpl.swift
@@ -55,6 +55,7 @@ private struct BackendInfoResponse: Decodable, ToAPIModelConvertible {
.init(
domain: domain,
isFederationEnabled: federation,
+ isMLSEnabled: false,
supportedVersions: Set(supported.compactMap(APIVersion.init)),
developmentVersions: Set(development?.compactMap(APIVersion.init) ?? [])
)
diff --git a/WireAPI/Sources/WireAPI/APIs/ConnectionsAPI/ConnectionsAPIBuilder.swift b/WireAPI/Sources/WireAPI/APIs/ConnectionsAPI/ConnectionsAPIBuilder.swift
index 143079d58a9..0e82fda2d8e 100644
--- a/WireAPI/Sources/WireAPI/APIs/ConnectionsAPI/ConnectionsAPIBuilder.swift
+++ b/WireAPI/Sources/WireAPI/APIs/ConnectionsAPI/ConnectionsAPIBuilder.swift
@@ -53,6 +53,8 @@ public struct ConnectionsAPIBuilder {
ConnectionsAPIV5(httpClient: httpClient)
case .v6:
ConnectionsAPIV6(httpClient: httpClient)
+ case .v7:
+ ConnectionsAPIV7(httpClient: httpClient)
}
}
diff --git a/WireAPI/Sources/WireAPI/APIs/ConnectionsAPI/ConnectionsAPIV7.swift b/WireAPI/Sources/WireAPI/APIs/ConnectionsAPI/ConnectionsAPIV7.swift
new file mode 100644
index 00000000000..f9d28ffa97a
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/ConnectionsAPI/ConnectionsAPIV7.swift
@@ -0,0 +1,21 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+final class ConnectionsAPIV7: ConnectionsAPIV6 {
+ override var apiVersion: APIVersion { .v7 }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPI.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPI.swift
index cbd84f28e35..2d2ab03653b 100644
--- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPI.swift
+++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPI.swift
@@ -41,4 +41,12 @@ public protocol ConversationsAPI {
in domain: String
) async throws -> Conversation
+ /// Fetches the guest link for a given conversation.
+ /// - parameter conversationID: The conversation identifier.
+ /// - returns: The conversation guest link.
+
+ func getConversationGuestLink(
+ conversationID: String
+ ) async throws -> String?
+
}
diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIBuilder.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIBuilder.swift
index 2d638680564..ced67191ca9 100644
--- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIBuilder.swift
+++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIBuilder.swift
@@ -21,13 +21,14 @@ import Foundation
/// Builder for the conversations API.
public struct ConversationsAPIBuilder {
- private let httpClient: any HTTPClient
+ let apiService: any APIServiceProtocol
- /// Create a new builder for the conversations API.
+ /// Create a new builder.
///
- /// - Parameter httpClient: A http client.
- public init(httpClient: any HTTPClient) {
- self.httpClient = httpClient
+ /// - Parameter APIService: An api service.
+
+ public init(apiService: any APIServiceProtocol) {
+ self.apiService = apiService
}
/// Make a versioned `ConversationsAPI`.
@@ -37,19 +38,21 @@ public struct ConversationsAPIBuilder {
public func makeAPI(for version: APIVersion) -> any ConversationsAPI {
switch version {
case .v0:
- ConversationsAPIV0(httpClient: httpClient)
+ ConversationsAPIV0(apiService: apiService)
case .v1:
- ConversationsAPIV1(httpClient: httpClient)
+ ConversationsAPIV1(apiService: apiService)
case .v2:
- ConversationsAPIV2(httpClient: httpClient)
+ ConversationsAPIV2(apiService: apiService)
case .v3:
- ConversationsAPIV3(httpClient: httpClient)
+ ConversationsAPIV3(apiService: apiService)
case .v4:
- ConversationsAPIV4(httpClient: httpClient)
+ ConversationsAPIV4(apiService: apiService)
case .v5:
- ConversationsAPIV5(httpClient: httpClient)
+ ConversationsAPIV5(apiService: apiService)
case .v6:
- ConversationsAPIV6(httpClient: httpClient)
+ ConversationsAPIV6(apiService: apiService)
+ case .v7:
+ ConversationsAPIV7(apiService: apiService)
}
}
diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIError.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIError.swift
index 2e0f8d3469a..adf98cd43e4 100644
--- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIError.swift
+++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIError.swift
@@ -19,6 +19,9 @@
/// Errors originating from `ConversationsAPI`.
public enum ConversationsAPIError: Error {
+ /// A request url is invalid.
+ case invalidURL
+
/// Failure if functionality has not been implemented.
case notImplemented
@@ -37,4 +40,19 @@ public enum ConversationsAPIError: Error {
/// Failure if user and domain are empty
case userAndDomainShouldNotBeEmpty
+ /// Access denied
+ case accessDenied
+
+ /// Conversation not found
+ case conversationNotFound
+
+ /// Conversation code not found
+ case conversationCodeNotFound
+
+ /// Conversation guests links disabled
+ case guestLinksDisabled
+
+ /// Invalid conversation id
+ case invalidConversationID
+
}
diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV0.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV0.swift
index e2b33fa8802..4387aa55472 100644
--- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV0.swift
+++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV0.swift
@@ -28,14 +28,18 @@ class ConversationsAPIV0: ConversationsAPI, VersionedAPI {
// MARK: - Properties
+ let apiService: any APIServiceProtocol
+
var apiVersion: APIVersion { .v0 }
- let httpClient: any HTTPClient
+ var basePath: String {
+ "/conversations"
+ }
// MARK: - Initialize
- init(httpClient: any HTTPClient) {
- self.httpClient = httpClient
+ init(apiService: any APIServiceProtocol) {
+ self.apiService = apiService
}
func getLegacyConversationIdentifiers() async throws -> PayloadPager {
@@ -48,24 +52,32 @@ class ConversationsAPIV0: ConversationsAPI, VersionedAPI {
// As soon as APIVersion.v0 is removed, the legacy function can be deleted, making the code clean and easy to
// understand.
- let resourcePath = "/conversations/list-ids/"
+ let components = URLComponents(string: "\(basePath)/list-ids/")
let jsonEncoder = JSONEncoder.defaultEncoder
+ guard let url = components?.url else {
+ assertionFailure("generated an invalid url")
+ throw ConversationsAPIError.invalidURL
+ }
+
return PayloadPager { start in
// body Params
let params = PaginationRequest(pagingState: start, size: Constants.batchSize)
let body = try jsonEncoder.encode(params)
- let request = HTTPRequest(
- path: resourcePath,
- method: .post,
- body: body
+ let request = URLRequestBuilder(url: url)
+ .withMethod(.post)
+ .withBody(body, contentType: .json)
+ .build()
+
+ let (data, response) = try await self.apiService.executeRequest(
+ request,
+ requiringAccessToken: true
)
- let response = try await self.httpClient.executeRequest(request)
return try ResponseParser()
.success(code: .ok, type: PaginatedConversationIDsV0.self)
- .parse(response)
+ .parse(code: response.statusCode, data: data)
}
}
@@ -77,19 +89,27 @@ class ConversationsAPIV0: ConversationsAPI, VersionedAPI {
func getConversations(for identifiers: [QualifiedID]) async throws -> ConversationList {
let parameters = GetConversationsParametersV0(qualifiedIdentifiers: identifiers)
let body = try JSONEncoder.defaultEncoder.encode(parameters)
- let resourcePath = "\(pathPrefix)/conversations/list/v2"
+ var components = URLComponents(string: "\(pathPrefix)\(basePath)/list/v2")
+
+ guard let url = components?.url else {
+ assertionFailure("generated an invalid url")
+ throw ConversationsAPIError.invalidURL
+ }
+
+ let request = URLRequestBuilder(url: url)
+ .withMethod(.post)
+ .withBody(body, contentType: .json)
+ .build()
- let request = HTTPRequest(
- path: resourcePath,
- method: .post,
- body: body
+ let (data, response) = try await apiService.executeRequest(
+ request,
+ requiringAccessToken: true
)
- let response = try await httpClient.executeRequest(request)
return try ResponseParser()
.success(code: .ok, type: QualifiedConversationListV0.self)
.failure(code: .badRequest, error: ConversationsAPIError.invalidBody)
- .parse(response)
+ .parse(code: response.statusCode, data: data)
}
func getMLSOneToOneConversation(
@@ -98,6 +118,40 @@ class ConversationsAPIV0: ConversationsAPI, VersionedAPI {
) async throws -> Conversation {
throw ConversationsAPIError.unsupportedEndpointForAPIVersion
}
+
+ func getConversationGuestLink(
+ conversationID: String
+ ) async throws -> String? {
+ let components = URLComponents(string: "\(pathPrefix)\(basePath)/\(conversationID)/code")
+
+ guard let url = components?.url else {
+ assertionFailure("generated an invalid url")
+ throw ConversationsAPIError.invalidURL
+ }
+
+ let request = URLRequestBuilder(url: url)
+ .withMethod(.get)
+ .build()
+
+ let (data, response) = try await apiService.executeRequest(
+ request,
+ requiringAccessToken: true
+ )
+
+ return try ResponseParser()
+ .success(code: .ok, type: ConversationCodeV0.self)
+ .failure(code: .forbidden, label: "access-denied", error: ConversationsAPIError.accessDenied)
+ .failure(code: .notFound, label: "cnv", error: ConversationsAPIError.invalidConversationID)
+ .failure(code: .notFound, label: "no-conversation", error: ConversationsAPIError.conversationNotFound)
+ .failure(
+ code: .notFound,
+ label: "no-conversation-code",
+ error: ConversationsAPIError.conversationCodeNotFound
+ )
+ .failure(code: .conflict, label: "guest-links-disabled", error: ConversationsAPIError.guestLinksDisabled)
+ .parse(code: response.statusCode, data: data)
+ }
+
}
// MARK: Encodables
@@ -220,3 +274,14 @@ struct ConversationV0: Decodable, ToAPIModelConvertible {
)
}
}
+
+struct ConversationCodeV0: Decodable, ToAPIModelConvertible {
+
+ let code: String
+ let key: String
+ let uri: String?
+
+ func toAPIModel() -> String? {
+ uri
+ }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV1.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV1.swift
index d5870b4c04e..0cd9ce59b5c 100644
--- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV1.swift
+++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV1.swift
@@ -27,24 +27,32 @@ class ConversationsAPIV1: ConversationsAPIV0 {
}
override func getConversationIdentifiers() async throws -> PayloadPager {
- let resourcePath = "\(pathPrefix)/conversations/list-ids/"
+ let components = URLComponents(string: "\(pathPrefix)\(basePath)/list-ids/")
let jsonEncoder = JSONEncoder.defaultEncoder
+ guard let url = components?.url else {
+ assertionFailure("generated an invalid url")
+ throw ConversationsAPIError.invalidURL
+ }
+
return PayloadPager { start in
// body Params
let params = PaginationRequest(pagingState: start, size: Constants.batchSize)
let body = try jsonEncoder.encode(params)
- let request = HTTPRequest(
- path: resourcePath,
- method: .post,
- body: body
+ let request = URLRequestBuilder(url: url)
+ .withMethod(.post)
+ .withBody(body, contentType: .json)
+ .build()
+
+ let (data, response) = try await self.apiService.executeRequest(
+ request,
+ requiringAccessToken: true
)
- let response = try await self.httpClient.executeRequest(request)
return try ResponseParser()
.success(code: .ok, type: PaginatedConversationIDsV1.self)
- .parse(response)
+ .parse(code: response.statusCode, data: data)
}
}
}
diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV2.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV2.swift
index 99e53964446..aa003733fa6 100644
--- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV2.swift
+++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV2.swift
@@ -26,18 +26,26 @@ class ConversationsAPIV2: ConversationsAPIV1 {
let body = try JSONEncoder.defaultEncoder.encode(parameters)
// New change for v2
- let resourcePath = "\(pathPrefix)/conversations/list"
+ let components = URLComponents(string: "\(pathPrefix)\(basePath)/list")
- let request = HTTPRequest(
- path: resourcePath,
- method: .post,
- body: body
+ guard let url = components?.url else {
+ assertionFailure("generated an invalid url")
+ throw ConversationsAPIError.invalidURL
+ }
+
+ let request = URLRequestBuilder(url: url)
+ .withMethod(.post)
+ .withBody(body, contentType: .json)
+ .build()
+
+ let (data, response) = try await apiService.executeRequest(
+ request,
+ requiringAccessToken: true
)
- let response = try await httpClient.executeRequest(request)
return try ResponseParser()
.success(code: .ok, type: QualifiedConversationListV0.self)
.failure(code: .badRequest, error: ConversationsAPIError.invalidBody)
- .parse(response)
+ .parse(code: response.statusCode, data: data)
}
}
diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV3.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV3.swift
index 07846885e79..41c1e575573 100644
--- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV3.swift
+++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV3.swift
@@ -24,19 +24,28 @@ class ConversationsAPIV3: ConversationsAPIV2 {
override func getConversations(for identifiers: [QualifiedID]) async throws -> ConversationList {
let parameters = GetConversationsParametersV0(qualifiedIdentifiers: identifiers)
let body = try JSONEncoder.defaultEncoder.encode(parameters)
- let resourcePath = "\(pathPrefix)/conversations/list"
- let request = HTTPRequest(
- path: resourcePath,
- method: .post,
- body: body
+ let components = URLComponents(string: "\(pathPrefix)\(basePath)/list")
+
+ guard let url = components?.url else {
+ assertionFailure("generated an invalid url")
+ throw ConversationsAPIError.invalidURL
+ }
+
+ let request = URLRequestBuilder(url: url)
+ .withMethod(.post)
+ .withBody(body, contentType: .json)
+ .build()
+
+ let (data, response) = try await apiService.executeRequest(
+ request,
+ requiringAccessToken: true
)
- let response = try await httpClient.executeRequest(request)
return try ResponseParser()
.success(code: .ok, type: QualifiedConversationListV3.self) // Change in v3
.failure(code: .badRequest, error: ConversationsAPIError.invalidBody)
- .parse(response)
+ .parse(code: response.statusCode, data: data)
}
}
diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV4.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV4.swift
index fae46fb57db..2b80f4109ce 100644
--- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV4.swift
+++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV4.swift
@@ -16,6 +16,64 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
+import Foundation
+
class ConversationsAPIV4: ConversationsAPIV3 {
override var apiVersion: APIVersion { .v4 }
+
+ override func getConversationGuestLink(
+ conversationID: String
+ ) async throws -> String? {
+ let components = URLComponents(string: "\(pathPrefix)\(basePath)/\(conversationID)/code")
+
+ guard let url = components?.url else {
+ assertionFailure("generated an invalid url")
+ throw ConversationsAPIError.invalidURL
+ }
+
+ let request = URLRequestBuilder(url: url)
+ .withMethod(.get)
+ .build()
+
+ let (data, response) = try await apiService.executeRequest(
+ request,
+ requiringAccessToken: true
+ )
+
+ return try ResponseParser()
+ .success(code: .ok, type: ConversationCodeV4.self) // New change in v4
+ .failure(
+ code: .badRequest,
+ label: "cnv",
+ error: ConversationsAPIError.invalidConversationID
+ ) // Dedicated error code in v4
+ .failure(code: .forbidden, label: "access-denied", error: ConversationsAPIError.accessDenied)
+ .failure(code: .notFound, label: "no-conversation", error: ConversationsAPIError.conversationNotFound)
+ .failure(
+ code: .notFound,
+ label: "no-conversation-code",
+ error: ConversationsAPIError.conversationCodeNotFound
+ )
+ .failure(code: .conflict, label: "guest-links-disabled", error: ConversationsAPIError.guestLinksDisabled)
+ .parse(code: response.statusCode, data: data)
+ }
+}
+
+struct ConversationCodeV4: Decodable, ToAPIModelConvertible {
+
+ let code: String
+ let hasPassword: Bool // Introduced in v4
+ let key: String
+ let uri: String?
+
+ enum CodingKeys: String, CodingKey {
+ case code
+ case hasPassword = "has_password"
+ case key
+ case uri
+ }
+
+ func toAPIModel() -> String? {
+ uri
+ }
}
diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV5.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV5.swift
index 1c743c5bddd..11bd05ca5e3 100644
--- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV5.swift
+++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV5.swift
@@ -21,22 +21,34 @@ import Foundation
class ConversationsAPIV5: ConversationsAPIV4 {
override var apiVersion: APIVersion { .v5 }
+ var oneToOneConversationsPath: String {
+ "\(pathPrefix)\(basePath)/one2one"
+ }
+
override func getConversations(for identifiers: [QualifiedID]) async throws -> ConversationList {
let parameters = GetConversationsParametersV0(qualifiedIdentifiers: identifiers)
let body = try JSONEncoder.defaultEncoder.encode(parameters)
- let resourcePath = "\(pathPrefix)/conversations/list"
+ let components = URLComponents(string: "\(pathPrefix)\(basePath)/list")
+
+ guard let url = components?.url else {
+ assertionFailure("generated an invalid url")
+ throw ConversationsAPIError.invalidURL
+ }
+
+ let request = URLRequestBuilder(url: url)
+ .withMethod(.post)
+ .withBody(body, contentType: .json)
+ .build()
- let request = HTTPRequest(
- path: resourcePath,
- method: .post,
- body: body
+ let (data, response) = try await apiService.executeRequest(
+ request,
+ requiringAccessToken: true
)
- let response = try await httpClient.executeRequest(request)
// Removed in v5: remove handling of error code 400
return try ResponseParser()
- .success(code: .ok, type: QualifiedConversationListV5.self) // Change in v5
- .parse(response)
+ .success(code: .ok, type: QualifiedConversationListV5.self)
+ .parse(code: response.statusCode, data: data)
}
override func getMLSOneToOneConversation(
@@ -47,20 +59,27 @@ class ConversationsAPIV5: ConversationsAPIV4 {
throw ConversationsAPIError.userAndDomainShouldNotBeEmpty
}
- let resourcePath = "\(pathPrefix)/conversations/one2one/\(domain)/\(userID)"
+ let components = URLComponents(string: "\(oneToOneConversationsPath)/\(domain)/\(userID)")
- let request = HTTPRequest(
- path: resourcePath,
- method: .get
- )
+ guard let url = components?.url else {
+ assertionFailure("generated an invalid url")
+ throw ConversationsAPIError.invalidURL
+ }
- let response = try await httpClient.executeRequest(request)
+ let request = URLRequestBuilder(url: url)
+ .withMethod(.get)
+ .build()
+
+ let (data, response) = try await apiService.executeRequest(
+ request,
+ requiringAccessToken: true
+ )
return try ResponseParser()
.success(code: .ok, type: ConversationV5.self)
.failure(code: .badRequest, label: "mls-not-enabled", error: ConversationsAPIError.mlsNotEnabled)
.failure(code: .forbidden, label: "not-connected", error: ConversationsAPIError.usersNotConnected)
- .parse(response)
+ .parse(code: response.statusCode, data: data)
}
}
diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV6.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV6.swift
index dc3beee99c6..9072f6ff151 100644
--- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV6.swift
+++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV6.swift
@@ -16,6 +16,6 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-final class ConversationsAPIV6: ConversationsAPIV5 {
+class ConversationsAPIV6: ConversationsAPIV5 {
override var apiVersion: APIVersion { .v6 }
}
diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV7.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV7.swift
new file mode 100644
index 00000000000..b9aed645bd8
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV7.swift
@@ -0,0 +1,26 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+final class ConversationsAPIV7: ConversationsAPIV6 {
+
+ override var apiVersion: APIVersion { .v7 }
+
+ override var oneToOneConversationsPath: String {
+ "\(pathPrefix)/one2one-conversations"
+ }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/FeatureConfigsAPI/FeatureConfigsAPIBuilder.swift b/WireAPI/Sources/WireAPI/APIs/FeatureConfigsAPI/FeatureConfigsAPIBuilder.swift
index 7f990077ad9..62f5bce7ad6 100644
--- a/WireAPI/Sources/WireAPI/APIs/FeatureConfigsAPI/FeatureConfigsAPIBuilder.swift
+++ b/WireAPI/Sources/WireAPI/APIs/FeatureConfigsAPI/FeatureConfigsAPIBuilder.swift
@@ -53,7 +53,8 @@ public struct FeatureConfigsAPIBuilder {
FeatureConfigsAPIV5(httpClient: httpClient)
case .v6:
FeatureConfigsAPIV6(httpClient: httpClient)
+ case .v7:
+ FeatureConfigsAPIV7(httpClient: httpClient)
}
}
-
}
diff --git a/WireAPI/Sources/WireAPI/APIs/FeatureConfigsAPI/FeatureConfigsAPIV7.swift b/WireAPI/Sources/WireAPI/APIs/FeatureConfigsAPI/FeatureConfigsAPIV7.swift
new file mode 100644
index 00000000000..8083b0ee3bc
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/FeatureConfigsAPI/FeatureConfigsAPIV7.swift
@@ -0,0 +1,21 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+final class FeatureConfigsAPIV7: FeatureConfigsAPIV6 {
+ override var apiVersion: APIVersion { .v7 }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/SelfUserAPI/SelfUserAPIBuilder.swift b/WireAPI/Sources/WireAPI/APIs/SelfUserAPI/SelfUserAPIBuilder.swift
index d072e1c234a..a64207d1bc3 100644
--- a/WireAPI/Sources/WireAPI/APIs/SelfUserAPI/SelfUserAPIBuilder.swift
+++ b/WireAPI/Sources/WireAPI/APIs/SelfUserAPI/SelfUserAPIBuilder.swift
@@ -53,6 +53,8 @@ public struct SelfUserAPIBuilder {
SelfUserAPIV5(httpClient: httpClient)
case .v6:
SelfUserAPIV6(httpClient: httpClient)
+ case .v7:
+ SelfUserAPIV7(httpClient: httpClient)
}
}
diff --git a/WireAPI/Sources/WireAPI/APIs/SelfUserAPI/SelfUserAPIV7.swift b/WireAPI/Sources/WireAPI/APIs/SelfUserAPI/SelfUserAPIV7.swift
new file mode 100644
index 00000000000..e6c44c2bc5f
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/SelfUserAPI/SelfUserAPIV7.swift
@@ -0,0 +1,21 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+final class SelfUserAPIV7: SelfUserAPIV6 {
+ override var apiVersion: APIVersion { .v7 }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPI.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPI.swift
index 433751b043b..51fb3f1a354 100644
--- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPI.swift
+++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPI.swift
@@ -51,16 +51,16 @@ public protocol TeamsAPI {
maxResults: UInt
) async throws -> [TeamMember]
- /// Get the legalhold status of a team member.
+ /// Get the legalhold of a team member.
///
/// - Parameters:
/// - teamID: The id of the team.
/// - userID: The id of the member.
- /// - Returns: The legalhold status of the member.
+ /// - Returns: The legalhold of the member.
- func getLegalholdStatus(
+ func getLegalholdInfo(
for teamID: Team.ID,
userID: UUID
- ) async throws -> LegalholdStatus
+ ) async throws -> TeamMemberLegalholdInfo
}
diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIBuilder.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIBuilder.swift
index b37717cec57..fe4ce3da859 100644
--- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIBuilder.swift
+++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIBuilder.swift
@@ -53,7 +53,8 @@ public struct TeamsAPIBuilder {
TeamsAPIV5(httpClient: httpClient)
case .v6:
TeamsAPIV6(httpClient: httpClient)
+ case .v7:
+ TeamsAPIV7(httpClient: httpClient)
}
}
-
}
diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift
index d83a9dc193e..85fb180df69 100644
--- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift
+++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift
@@ -96,12 +96,12 @@ class TeamsAPIV0: TeamsAPI, VersionedAPI {
.parse(response)
}
- // MARK: - Get legalhold status
+ // MARK: - Get team member legalhold
- func getLegalholdStatus(
+ func getLegalholdInfo(
for teamID: Team.ID,
userID: UUID
- ) async throws -> LegalholdStatus {
+ ) async throws -> TeamMemberLegalholdInfo {
let request = HTTPRequest(
path: "\(basePath(for: teamID))/legalhold/\(userID.transportString())",
method: .get
@@ -110,7 +110,7 @@ class TeamsAPIV0: TeamsAPI, VersionedAPI {
let response = try await httpClient.executeRequest(request)
return try ResponseParser()
- .success(code: .ok, type: LegalholdStatusResponseV0.self)
+ .success(code: .ok, type: TeamMemberLegalholdResponseV0.self)
.failure(code: .notFound, error: TeamsAPIError.invalidRequest)
.failure(code: .notFound, label: "no-team-member", error: TeamsAPIError.teamMemberNotFound)
.parse(response)
@@ -301,15 +301,35 @@ enum LegalholdStatusV0: String, Decodable {
.noConsent
}
}
+}
+
+struct LegalHoldLastPrekeyV0: Decodable, ToAPIModelConvertible {
+ let id: Int
+ let key: String
+ func toAPIModel() -> Prekey {
+ Prekey(
+ id: id,
+ base64EncodedKey: key
+ )
+ }
}
-struct LegalholdStatusResponseV0: Decodable, ToAPIModelConvertible {
+struct TeamMemberLegalholdResponseV0: Decodable, ToAPIModelConvertible {
+ let lastPrekey: LegalHoldLastPrekeyV0
let status: LegalholdStatusV0
- func toAPIModel() -> LegalholdStatus {
- status.toAPIModel()
+ enum CodingKeys: String, CodingKey {
+ case status
+ case lastPrekey = "last_prekey"
+ }
+
+ func toAPIModel() -> TeamMemberLegalholdInfo {
+ TeamMemberLegalholdInfo(
+ status: status.toAPIModel(),
+ prekey: lastPrekey.toAPIModel()
+ )
}
}
diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift
index 686ed0ef421..e0f2cd5e68a 100644
--- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift
+++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift
@@ -89,12 +89,12 @@ class TeamsAPIV4: TeamsAPIV3 {
.parse(response)
}
- // MARK: - Get legalhold status
+ // MARK: - Get team member legalhold
- override func getLegalholdStatus(
+ override func getLegalholdInfo(
for teamID: Team.ID,
userID: UUID
- ) async throws -> LegalholdStatus {
+ ) async throws -> TeamMemberLegalholdInfo {
let request = HTTPRequest(
path: "\(basePath(for: teamID))/legalhold/\(userID.transportString())",
method: .get
@@ -104,7 +104,7 @@ class TeamsAPIV4: TeamsAPIV3 {
// New: 400
return try ResponseParser()
- .success(code: .ok, type: LegalholdStatusResponseV0.self)
+ .success(code: .ok, type: TeamMemberLegalholdResponseV0.self)
.failure(code: .badRequest, error: TeamsAPIError.invalidRequest)
.failure(code: .notFound, label: "no-team-member", error: TeamsAPIError.teamMemberNotFound)
.parse(response)
diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift
index 48293daf146..715f95a78db 100644
--- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift
+++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift
@@ -86,12 +86,12 @@ class TeamsAPIV5: TeamsAPIV4 {
.parse(response)
}
- // MARK: - Get legalhold status
+ // MARK: - Get team member legalhold
- override func getLegalholdStatus(
+ override func getLegalholdInfo(
for teamID: Team.ID,
userID: UUID
- ) async throws -> LegalholdStatus {
+ ) async throws -> TeamMemberLegalholdInfo {
let request = HTTPRequest(
path: "\(basePath(for: teamID))/legalhold/\(userID.transportString())",
method: .get
@@ -101,7 +101,7 @@ class TeamsAPIV5: TeamsAPIV4 {
// New: 404 invalid request.
return try ResponseParser()
- .success(code: .ok, type: LegalholdStatusResponseV0.self)
+ .success(code: .ok, type: TeamMemberLegalholdResponseV0.self)
.failure(code: .notFound, error: TeamsAPIError.invalidRequest)
.failure(code: .notFound, label: "no-team-member", error: TeamsAPIError.teamMemberNotFound)
.parse(response)
diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV7.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV7.swift
new file mode 100644
index 00000000000..8f90f4e9425
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV7.swift
@@ -0,0 +1,21 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+final class TeamsAPIV7: TeamsAPIV6 {
+ override var apiVersion: APIVersion { .v7 }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Event decoding/Conversation/ConversationMemberLeaveEventDecoder.swift b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Event decoding/Conversation/ConversationMemberLeaveEventDecoder.swift
index 4de08eeae45..c303e36476e 100644
--- a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Event decoding/Conversation/ConversationMemberLeaveEventDecoder.swift
+++ b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Event decoding/Conversation/ConversationMemberLeaveEventDecoder.swift
@@ -48,7 +48,7 @@ struct ConversationMemberLeaveEventDecoder {
senderID: senderID,
timestamp: timestamp.date,
removedUserIDs: payload.userIDs,
- reason: payload.reason ?? .left
+ reason: payload.reason ?? .userLeft
)
}
diff --git a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Event decoding/Conversation/ConversationReceiptModeUpdateEventDecoder.swift b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Event decoding/Conversation/ConversationReceiptModeUpdateEventDecoder.swift
index aedb8128342..9c0c62cfd24 100644
--- a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Event decoding/Conversation/ConversationReceiptModeUpdateEventDecoder.swift
+++ b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Event decoding/Conversation/ConversationReceiptModeUpdateEventDecoder.swift
@@ -41,7 +41,7 @@ struct ConversationReceiptModeUpdateEventDecoder {
return ConversationReceiptModeUpdateEvent(
conversationID: conversationID,
senderID: senderID,
- newRecieptMode: payload.receiptMode
+ newReceiptMode: payload.receiptMode
)
}
diff --git a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Responses/UpdateEventEnvelopeV0.swift b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Responses/UpdateEventEnvelopeV0.swift
index 3a9b4682da0..30d51fd5bff 100644
--- a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Responses/UpdateEventEnvelopeV0.swift
+++ b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Responses/UpdateEventEnvelopeV0.swift
@@ -18,7 +18,9 @@
import Foundation
-struct UpdateEventEnvelopeV0: Decodable, ToAPIModelConvertible {
+// TODO: [WPB-9612] make internal
+// This is public for testing purposes.
+public struct UpdateEventEnvelopeV0: Decodable, ToAPIModelConvertible {
let id: UUID
let payload: [UpdateEventDecodingProxy]?
diff --git a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/UpdateEventsAPIBuilder.swift b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/UpdateEventsAPIBuilder.swift
index 4a41613b19e..65caf64ed4a 100644
--- a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/UpdateEventsAPIBuilder.swift
+++ b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/UpdateEventsAPIBuilder.swift
@@ -53,6 +53,8 @@ public struct UpdateEventsAPIBuilder {
UpdateEventsAPIV5(apiService: apiService)
case .v6:
UpdateEventsAPIV6(apiService: apiService)
+ case .v7:
+ UpdateEventsAPIV7(apiService: apiService)
}
}
diff --git a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/UpdateEventsAPIError.swift b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/UpdateEventsAPIError.swift
index 9989e915635..4745ff3f764 100644
--- a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/UpdateEventsAPIError.swift
+++ b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/UpdateEventsAPIError.swift
@@ -22,7 +22,7 @@ import Foundation
public enum UpdateEventsAPIError: Error {
- /// A request url is not invalid.
+ /// A request url is invalid.
case invalidURL
diff --git a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/UpdateEventsAPIV7.swift b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/UpdateEventsAPIV7.swift
new file mode 100644
index 00000000000..ae23f154228
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/UpdateEventsAPIV7.swift
@@ -0,0 +1,21 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+final class UpdateEventsAPIV7: UpdateEventsAPIV6 {
+ override var apiVersion: APIVersion { .v7 }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/UserClientsAPI/UserClientsAPIBuilder.swift b/WireAPI/Sources/WireAPI/APIs/UserClientsAPI/UserClientsAPIBuilder.swift
index 2d2366c52fb..b15d5045af4 100644
--- a/WireAPI/Sources/WireAPI/APIs/UserClientsAPI/UserClientsAPIBuilder.swift
+++ b/WireAPI/Sources/WireAPI/APIs/UserClientsAPI/UserClientsAPIBuilder.swift
@@ -53,6 +53,8 @@ public struct UserClientsAPIBuilder {
UserClientsAPIV5(apiService: apiService)
case .v6:
UserClientsAPIV6(apiService: apiService)
+ case .v7:
+ UserClientsAPIV7(apiService: apiService)
}
}
diff --git a/WireAPI/Sources/WireAPI/APIs/UserClientsAPI/UserClientsAPIError.swift b/WireAPI/Sources/WireAPI/APIs/UserClientsAPI/UserClientsAPIError.swift
index 0dce6180081..01017288662 100644
--- a/WireAPI/Sources/WireAPI/APIs/UserClientsAPI/UserClientsAPIError.swift
+++ b/WireAPI/Sources/WireAPI/APIs/UserClientsAPI/UserClientsAPIError.swift
@@ -22,7 +22,7 @@ import Foundation
public enum UserClientsAPIError: Error {
- /// A request url is not invalid.
+ /// A request url is invalid.
case invalidURL
}
diff --git a/WireAPI/Sources/WireAPI/APIs/UserClientsAPI/UserClientsAPIV7.swift b/WireAPI/Sources/WireAPI/APIs/UserClientsAPI/UserClientsAPIV7.swift
new file mode 100644
index 00000000000..6d989837898
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/UserClientsAPI/UserClientsAPIV7.swift
@@ -0,0 +1,108 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+
+final class UserClientsAPIV7: UserClientsAPIV6 {
+
+ override var apiVersion: APIVersion { .v7 }
+
+ override func getSelfClients() async throws -> [SelfUserClient] {
+ let components = URLComponents(string: "\(pathPrefix)/clients")
+
+ guard let url = components?.url else {
+ assertionFailure("generated an invalid url")
+ throw UserClientsAPIError.invalidURL
+ }
+
+ let request = URLRequestBuilder(url: url)
+ .withMethod(.get)
+ .build()
+
+ let (data, response) = try await apiService.executeRequest(
+ request,
+ requiringAccessToken: true
+ )
+
+ return try ResponseParser()
+ .success(code: .ok, type: ListUserClientV7.self)
+ .parse(code: response.statusCode, data: data)
+ }
+}
+
+// SelfUserClientV7.capabilities is now a list and not nested within another object anymore.
+
+private struct ListUserClientV7: Decodable, ToAPIModelConvertible {
+
+ let payload: [SelfUserClientV7]
+
+ init(from decoder: any Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ let payload = try container.decode([SelfUserClientV7].self)
+ self.payload = payload
+ }
+
+ func toAPIModel() -> [SelfUserClient] {
+ payload.map { $0.toAPIModel() }
+ }
+}
+
+private struct SelfUserClientV7: Decodable, ToAPIModelConvertible {
+
+ let id: String
+ let type: UserClientType
+ let activationDate: UTCTimeMillis
+ let label: String?
+ let model: String?
+ let deviceClass: DeviceClass?
+ let lastActiveDate: UTCTime?
+ let mlsPublicKeys: MLSPublicKeys?
+ let cookie: String?
+ let capabilities: [UserClientCapability]? // not of type `CapabilitiesList` anymore
+
+ enum CodingKeys: String, CodingKey {
+
+ case id
+ case type
+ case activationDate = "time"
+ case label
+ case model
+ case deviceClass = "class"
+ case lastActiveDate = "last_active"
+ case mlsPublicKeys = "mls_public_keys"
+ case cookie
+ case capabilities
+
+ }
+
+ func toAPIModel() -> SelfUserClient {
+ SelfUserClient(
+ id: id,
+ type: type,
+ activationDate: activationDate.date,
+ label: label,
+ model: model,
+ deviceClass: deviceClass,
+ lastActiveDate: lastActiveDate?.date,
+ mlsPublicKeys: mlsPublicKeys,
+ cookie: cookie,
+ capabilities: capabilities ?? []
+ )
+ }
+
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/UserPropertiesAPI/UserPropertiesAPIBuilder.swift b/WireAPI/Sources/WireAPI/APIs/UserPropertiesAPI/UserPropertiesAPIBuilder.swift
index 64bea8fe1c5..c31d910dee6 100644
--- a/WireAPI/Sources/WireAPI/APIs/UserPropertiesAPI/UserPropertiesAPIBuilder.swift
+++ b/WireAPI/Sources/WireAPI/APIs/UserPropertiesAPI/UserPropertiesAPIBuilder.swift
@@ -53,6 +53,8 @@ public struct UserPropertiesBuilder {
UserPropertiesAPIV5(httpClient: httpClient)
case .v6:
UserPropertiesAPIV6(httpClient: httpClient)
+ case .v7:
+ UserPropertiesAPIV7(httpClient: httpClient)
}
}
diff --git a/WireAPI/Sources/WireAPI/APIs/UserPropertiesAPI/UserPropertiesAPIV7.swift b/WireAPI/Sources/WireAPI/APIs/UserPropertiesAPI/UserPropertiesAPIV7.swift
new file mode 100644
index 00000000000..19efbe858f8
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/UserPropertiesAPI/UserPropertiesAPIV7.swift
@@ -0,0 +1,21 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+final class UserPropertiesAPIV7: UserPropertiesAPIV6 {
+ override var apiVersion: APIVersion { .v7 }
+}
diff --git a/WireAPI/Sources/WireAPI/APIs/UsersAPI/UsersAPIBuilder.swift b/WireAPI/Sources/WireAPI/APIs/UsersAPI/UsersAPIBuilder.swift
index d4fe40803c7..66325e9ff44 100644
--- a/WireAPI/Sources/WireAPI/APIs/UsersAPI/UsersAPIBuilder.swift
+++ b/WireAPI/Sources/WireAPI/APIs/UsersAPI/UsersAPIBuilder.swift
@@ -53,7 +53,8 @@ public struct UsersAPIBuilder {
UsersAPIV5(httpClient: httpClient)
case .v6:
UsersAPIV6(httpClient: httpClient)
+ case .v7:
+ UsersAPIV7(httpClient: httpClient)
}
}
-
}
diff --git a/WireAPI/Sources/WireAPI/APIs/UsersAPI/UsersAPIV7.swift b/WireAPI/Sources/WireAPI/APIs/UsersAPI/UsersAPIV7.swift
new file mode 100644
index 00000000000..d87bfd4b2ff
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/APIs/UsersAPI/UsersAPIV7.swift
@@ -0,0 +1,21 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+final class UsersAPIV7: UsersAPIV6 {
+ override var apiVersion: APIVersion { .v7 }
+}
diff --git a/WireAPI/Sources/WireAPI/Assembly.swift b/WireAPI/Sources/WireAPI/Assembly.swift
new file mode 100644
index 00000000000..9034110f09f
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/Assembly.swift
@@ -0,0 +1,93 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+import WireFoundation
+
+public final class Assembly {
+
+ let userID: UUID
+ let clientID: String
+ let backendEnvironment: BackendEnvironment
+ let minTLSVersion: TLSVersion
+ let cookieEncryptionKey: Data
+
+ public init(
+ userID: UUID,
+ clientID: String,
+ backendEnvironment: BackendEnvironment,
+ minTLSVersion: TLSVersion,
+ cookieEncryptionKey: Data
+ ) {
+ self.userID = userID
+ self.clientID = clientID
+ self.backendEnvironment = backendEnvironment
+ self.minTLSVersion = minTLSVersion
+ self.cookieEncryptionKey = cookieEncryptionKey
+ }
+
+ private lazy var keychain: some KeychainProtocol = Keychain()
+ private lazy var urlSessionConfigurationFactory = URLSessionConfigurationFactory(
+ minTLSVersion: minTLSVersion,
+ proxySettings: backendEnvironment.proxySettings
+ )
+
+ private lazy var apiService: some APIServiceProtocol = APIService(
+ networkService: apiNetworkService,
+ authenticationManager: authenticationManager
+ )
+
+ public lazy var apiNetworkService: NetworkService = {
+ let service = NetworkService(baseURL: backendEnvironment.url, serverTrustValidator: serverTrustValidator)
+ let config = urlSessionConfigurationFactory.makeRESTAPISessionConfiguration()
+ let session = URLSession(configuration: config, delegate: service, delegateQueue: nil)
+ service.configure(with: session)
+ return service
+ }()
+
+ private lazy var pushChannelService: some PushChannelServiceProtocol = PushChannelService(
+ networkService: pushChannelNetworkService,
+ authenticationManager: authenticationManager
+ )
+
+ private lazy var pushChannelNetworkService: NetworkService = {
+ let service = NetworkService(
+ baseURL: backendEnvironment.webSocketURL,
+ serverTrustValidator: serverTrustValidator
+ )
+ let config = urlSessionConfigurationFactory.makeWebSocketSessionConfiguration()
+ let session = URLSession(configuration: config, delegate: service, delegateQueue: nil)
+ service.configure(with: session)
+ return service
+ }()
+
+ public lazy var authenticationManager: some AuthenticationManagerProtocol = AuthenticationManager(
+ clientID: clientID,
+ cookieStorage: cookieStorage,
+ networkService: apiNetworkService
+ )
+
+ private lazy var cookieStorage: some CookieStorageProtocol = CookieStorage(
+ userID: userID,
+ cookieEncryptionKey: cookieEncryptionKey,
+ keychain: keychain
+ )
+
+ private lazy var serverTrustValidator = ServerTrustValidator(pinnedKeys: backendEnvironment.pinnedKeys)
+
+}
diff --git a/WireAPI/Sources/WireAPI/Authentication/AuthenticationManager.swift b/WireAPI/Sources/WireAPI/Authentication/AuthenticationManager.swift
index 98e24c933ae..e81fdf1e26c 100644
--- a/WireAPI/Sources/WireAPI/Authentication/AuthenticationManager.swift
+++ b/WireAPI/Sources/WireAPI/Authentication/AuthenticationManager.swift
@@ -20,7 +20,7 @@ import Foundation
import WireFoundation
// sourcery: AutoMockable
-protocol AuthenticationManagerProtocol {
+public protocol AuthenticationManagerProtocol {
func getValidAccessToken() async throws -> AccessToken
func refreshAccessToken() async throws -> AccessToken
diff --git a/WireAPI/Sources/WireAPI/Authentication/AuthenticationStorage.swift b/WireAPI/Sources/WireAPI/Authentication/AuthenticationStorage.swift
deleted file mode 100644
index 1521306b5e3..00000000000
--- a/WireAPI/Sources/WireAPI/Authentication/AuthenticationStorage.swift
+++ /dev/null
@@ -1,49 +0,0 @@
-//
-// Wire
-// Copyright (C) 2024 Wire Swiss GmbH
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see http://www.gnu.org/licenses/.
-//
-
-import Foundation
-
-/// Storage for authentication primitives.
-
-public protocol AuthenticationStorage {
-
- /// Store an access token.
- ///
- /// - Parameter accessToken: The token to store.
-
- func storeAccessToken(_ accessToken: AccessToken) async
-
- /// Fetch a stored access token.
- ///
- /// - Returns: The stored access token.
-
- func fetchAccessToken() async -> AccessToken?
-
- /// Store cookies.
- ///
- /// - Parameter cookies: The cookies to store.
-
- func storeCookies(_ cookies: [HTTPCookie]) async throws
-
- /// Fetch stored cookies.
- ///
- /// - Returns: The stored cookies.
-
- func fetchCookies() async throws -> [HTTPCookie]
-
-}
diff --git a/WireAPI/Sources/WireAPI/Components/ResponseParser.swift b/WireAPI/Sources/WireAPI/Components/ResponseParser.swift
index dcc210b6c44..2b42776eab5 100644
--- a/WireAPI/Sources/WireAPI/Components/ResponseParser.swift
+++ b/WireAPI/Sources/WireAPI/Components/ResponseParser.swift
@@ -73,7 +73,7 @@ struct ResponseParser {
func failure(
code: HTTPStatusCode,
label: String = "",
- error: any Error
+ error: some Error
) -> ResponseParser {
var copy = self
copy.parseBlocks.append { _, data in
diff --git a/WireAPI/Sources/WireAPI/HTTP Client/HTTPStatusCode.swift b/WireAPI/Sources/WireAPI/HTTP Client/HTTPStatusCode.swift
index 8f33ef98474..0285675e555 100644
--- a/WireAPI/Sources/WireAPI/HTTP Client/HTTPStatusCode.swift
+++ b/WireAPI/Sources/WireAPI/HTTP Client/HTTPStatusCode.swift
@@ -46,6 +46,10 @@ enum HTTPStatusCode: Int {
case forbidden = 403
+ /// conflict - 409
+
+ case conflict = 409
+
// MARK: Server Errors - 5xx
/// service unavailable - 503
diff --git a/WireAPI/Sources/WireAPI/Models/API/APIVersion.swift b/WireAPI/Sources/WireAPI/Models/API/APIVersion.swift
index 294168b52a8..5a25b166ca2 100644
--- a/WireAPI/Sources/WireAPI/Models/API/APIVersion.swift
+++ b/WireAPI/Sources/WireAPI/Models/API/APIVersion.swift
@@ -31,6 +31,7 @@ public enum APIVersion: UInt, CaseIterable, Comparable {
case v4
case v5
case v6
+ case v7
/// API versions considered production ready by the client.
///
@@ -42,7 +43,7 @@ public enum APIVersion: UInt, CaseIterable, Comparable {
/// Only if these critera are met should we explicitly mark the version
/// as production ready.
- public static let productionVersions: Set = [.v0, .v1, .v2, .v3, .v4, .v5]
+ public static let productionVersions: Set = [.v0, .v1, .v2, .v3, .v4, .v5, .v6]
/// API versions currently under development and not suitable for production
/// environments.
diff --git a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessor.swift b/WireAPI/Sources/WireAPI/Models/Accounts/UpgradedAccountTeam.swift
similarity index 58%
rename from WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessor.swift
rename to WireAPI/Sources/WireAPI/Models/Accounts/UpgradedAccountTeam.swift
index 08e9e898cea..f7060f87d5e 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessor.swift
+++ b/WireAPI/Sources/WireAPI/Models/Accounts/UpgradedAccountTeam.swift
@@ -16,25 +16,30 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import WireAPI
+import Foundation
-/// Process conversation typing events.
+/// The upgraded account's team information.
-protocol ConversationTypingEventProcessorProtocol {
+public struct UpgradedAccountTeam: Equatable, Sendable {
- /// Process a conversation typing event.
- ///
- /// - Parameter event: A conversation typing event.
-
- func processEvent(_ event: ConversationTypingEvent) async throws
+ /// The team's ID.
+ public let teamId: UUID
-}
+ /// The team's name.
+ public let teamName: String
-struct ConversationTypingEventProcessor: ConversationTypingEventProcessorProtocol {
-
- func processEvent(_: ConversationTypingEvent) async throws {
- // TODO: [WPB-10178]
- assertionFailure("not implemented yet")
+ /// Create a new `UpgradedAccountTeam`.
+ ///
+ /// - Parameters:
+ /// - teamId: The team's ID.
+ /// - teamName: The team's name.
+
+ public init(
+ teamId: UUID,
+ teamName: String
+ ) {
+ self.teamId = teamId
+ self.teamName = teamName
}
}
diff --git a/WireAPI/Sources/WireAPI/Models/Backend/BackendEnvironment.swift b/WireAPI/Sources/WireAPI/Models/Backend/BackendEnvironment.swift
new file mode 100644
index 00000000000..77377ad3acc
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/Models/Backend/BackendEnvironment.swift
@@ -0,0 +1,55 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+
+/// A collection of data for connecting to a given backend environment (e.g. Production, Staging, etc).
+
+public struct BackendEnvironment {
+
+ /// The `URL` of the backend.
+
+ let url: URL
+
+ /// The `URL` of the WebSocket endpoint.
+
+ let webSocketURL: URL
+
+ /// The pinned keys for the backend for use with certificate pinning.
+
+ let pinnedKeys: [PinnedKey]
+
+ /// The proxy settings for the backend if any.
+
+ let proxySettings: ProxySettings?
+
+ /// Creates a new `BackendEnvironment`.
+ ///
+ /// - Parameter url: The `URL` of the backend.
+ /// - Parameter webSocketURL: The `URL` of the WebSocket endpoint.
+ /// - Parameter pinnedKeys: The pinned keys for the backend for use with certificate pinning.
+ /// - Parameter proxySettings: The proxy settings for the backend if any.
+
+ public init(url: URL, webSocketURL: URL, pinnedKeys: [PinnedKey], proxySettings: ProxySettings?) {
+ self.url = url
+ self.webSocketURL = webSocketURL
+ self.pinnedKeys = pinnedKeys
+ self.proxySettings = proxySettings
+ }
+
+}
diff --git a/WireAPI/Sources/WireAPI/Models/Backend/BackendInfo.swift b/WireAPI/Sources/WireAPI/Models/Backend/BackendInfo.swift
index 1cb54c722f7..6af8106e845 100644
--- a/WireAPI/Sources/WireAPI/Models/Backend/BackendInfo.swift
+++ b/WireAPI/Sources/WireAPI/Models/Backend/BackendInfo.swift
@@ -30,6 +30,10 @@ public struct BackendInfo: Equatable {
public let isFederationEnabled: Bool
+ /// Whether the backend supports MLS.
+
+ public let isMLSEnabled: Bool
+
/// All production ready api versions supported by the local backend.
public let supportedVersions: Set
diff --git a/WireAPI/Sources/WireAPI/Models/Backend/PinnedKey.swift b/WireAPI/Sources/WireAPI/Models/Backend/PinnedKey.swift
new file mode 100644
index 00000000000..511d75a4dd0
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/Models/Backend/PinnedKey.swift
@@ -0,0 +1,74 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+@preconcurrency import Security
+
+/// Associates a list of `hosts` with a public `key`.
+
+public struct PinnedKey: Sendable {
+
+ public enum Failure: Error {
+ case invalidKeyData
+ }
+
+ public enum Host: Sendable {
+ case endsWith(String)
+ case equals(String)
+ }
+
+ let key: SecKey
+ let hosts: [Host]
+
+ public init(key: SecKey, hosts: [Host]) {
+ self.key = key
+ self.hosts = hosts
+ }
+
+ public init(key: Data, hosts: [Host]) throws(Failure) {
+ self.key = try Self.key(for: key)
+ self.hosts = hosts
+ }
+
+ /// Returns `true` if `host` matches any of the `hosts` in `self`.
+
+ func matches(host: String) -> Bool {
+ hosts.contains {
+ switch $0 {
+ case let .endsWith(suffix):
+ host.hasSuffix(suffix)
+ case let .equals(value):
+ host == value
+ }
+ }
+ }
+
+ // MARK: - Private
+
+ private static func key(for data: Data) throws(Failure) -> SecKey {
+ guard
+ let certificate = SecCertificateCreateWithData(nil, data as CFData),
+ let publicKey = SecCertificateCopyKey(certificate)
+ else {
+ throw Failure.invalidKeyData
+ }
+
+ return publicKey
+ }
+
+}
diff --git a/WireAPI/Sources/WireAPI/Models/Backend/ProxySettings.swift b/WireAPI/Sources/WireAPI/Models/Backend/ProxySettings.swift
new file mode 100644
index 00000000000..059eab1547d
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/Models/Backend/ProxySettings.swift
@@ -0,0 +1,60 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+
+/// Proxy settings for communicating with a backend server.
+
+public enum ProxySettings {
+
+ /// Settings for an unauthenticated proxy.
+
+ case unauthenticated(host: String, port: Int)
+
+ /// Settings for an authenticated proxy.
+
+ case authenticated(host: String, port: Int, username: String, password: String)
+
+ /// Dictionary to be used with `URLSessionConfiguration.connectionProxyDictionary`.
+
+ func proxyDictionary() -> [AnyHashable: Any] {
+ let socksEnable = "SOCKSEnable"
+ let socksProxy = "SOCKSProxy"
+ let socksPort = "SOCKSPort"
+
+ var result: [AnyHashable: Any] = [
+ socksEnable: 1,
+ kCFProxyTypeKey: kCFProxyTypeSOCKS,
+ kCFStreamPropertySOCKSVersion: kCFStreamSocketSOCKSVersion5
+ ]
+
+ switch self {
+ case let .unauthenticated(host, port):
+ result[socksProxy] = host
+ result[socksPort] = port
+ case let .authenticated(host, port, username, password):
+ result[socksProxy] = host
+ result[socksPort] = port
+ result[kCFStreamPropertySOCKSUser] = username
+ result[kCFStreamPropertySOCKSPassword] = password
+ }
+
+ return result
+ }
+
+}
diff --git a/WireAPI/Sources/WireAPI/Models/Backend/ServerTrustValidator.swift b/WireAPI/Sources/WireAPI/Models/Backend/ServerTrustValidator.swift
new file mode 100644
index 00000000000..011e7035c0a
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/Models/Backend/ServerTrustValidator.swift
@@ -0,0 +1,98 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+@preconcurrency import Security
+
+struct ServerTrustValidator: Sendable {
+
+ enum Failure: Error, Equatable {
+ case evaluatingServerTrustFailed
+ case noPublicKeyOnServerTrust
+ case noMatchingPublicKey
+ }
+
+ private let pinnedKeys: [PinnedKey]
+
+ init(pinnedKeys: [PinnedKey]) {
+ self.pinnedKeys = pinnedKeys
+ }
+
+ /// Verifies the server `trust` for the given `host`.
+ ///
+ /// - Parameter trust: The `SecTrust` of the server.
+ /// - Parameter host: The host of the server.
+ /// - Throws: An error if server certificate should not be trusted.
+ /// - Note: If no pinned keys are found for the `host`, the server certificate is trusted.
+
+ func validate(trust: SecTrust, host: String) async throws {
+ let matchingKeys = pinnedKeys.filter { $0.matches(host: host) }.map(\.key)
+
+ // If no keys are pinned for `host`, we trust the server certificate
+ guard !matchingKeys.isEmpty else { return }
+
+ try await Self.verifyServerCertificateTrusted(trust)
+
+ let publicKey = try Self.publicKeyAssociatedWithServerTrust(trust)
+
+ guard matchingKeys.contains(publicKey) else {
+ throw Failure.noMatchingPublicKey
+ }
+ }
+
+ // MARK: - Private
+
+ private static func verifyServerCertificateTrusted(_ serverTrust: SecTrust) async throws {
+ try await withCheckedThrowingContinuation { continuation in
+ // `SecTrustEvaluateAsyncWithError` requires the completion queue to be the same as the queue on which it
+ // is called.
+ let queue = DispatchQueue.global()
+ queue.async {
+ SecTrustEvaluateAsyncWithError(serverTrust, queue) { _, success, error in
+ if success {
+ continuation.resume()
+ } else {
+ print("Server trust evaluation failed: \(String(describing: error))")
+ continuation.resume(throwing: Failure.evaluatingServerTrustFailed)
+ }
+ }
+ }
+ }
+ }
+
+ /// Returns the public key of the leaf certificate associated with `serverTrust`.
+ ///
+ /// - Parameter serverTrust: SecTrust of server
+ /// - Returns: public key from `serverTrust`
+
+ private static func publicKeyAssociatedWithServerTrust(_ serverTrust: SecTrust) throws -> SecKey {
+ let certificates = SecTrustCopyCertificateChain(serverTrust) ?? [] as CFArray
+ let policy = SecPolicyCreateBasicX509()
+ var secTrust: SecTrust?
+
+ guard SecTrustCreateWithCertificates(certificates, policy, &secTrust) == noErr,
+ let trust = secTrust,
+ let result = SecTrustCopyKey(trust)
+ else {
+ throw Failure.noPublicKeyOnServerTrust
+ }
+
+ return result
+ }
+
+}
diff --git a/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMemberLeaveReason.swift b/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMemberLeaveReason.swift
index 176c2aecc3f..e5dc43ee6bc 100644
--- a/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMemberLeaveReason.swift
+++ b/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMemberLeaveReason.swift
@@ -29,10 +29,10 @@ public enum ConversationMemberLeaveReason: String, Codable, Sendable {
/// The user left the conversation by themselves.
- case left
+ case userLeft
/// The user was removed from the conversation by an admin.
- case removed
+ case userRemoved
}
diff --git a/WireAPI/Sources/WireAPI/Models/Messaging/MessageContent.swift b/WireAPI/Sources/WireAPI/Models/Messaging/MessageContent.swift
index 294b520f80c..c5096233f12 100644
--- a/WireAPI/Sources/WireAPI/Models/Messaging/MessageContent.swift
+++ b/WireAPI/Sources/WireAPI/Models/Messaging/MessageContent.swift
@@ -21,7 +21,7 @@ import Foundation
/// The contents of a message, typically as a base-64 encoded
/// Protobuf string.
-public enum MessageContent: Equatable, Codable {
+public enum MessageContent: Equatable, Codable, Sendable {
/// Encrypted message content.
diff --git a/WireAPI/Sources/WireAPI/Models/Network/TLSVersion.swift b/WireAPI/Sources/WireAPI/Models/Network/TLSVersion.swift
index 101460de87c..81aa816f5f5 100644
--- a/WireAPI/Sources/WireAPI/Models/Network/TLSVersion.swift
+++ b/WireAPI/Sources/WireAPI/Models/Network/TLSVersion.swift
@@ -30,6 +30,23 @@ public enum TLSVersion {
case v1_3
+ public static func minVersionFrom(_ string: String?) -> TLSVersion {
+ string.flatMap(TLSVersion.init) ?? .v1_2
+ }
+
+ public init?(_ string: String) {
+ switch string {
+ case "1.2":
+ self = .v1_2
+
+ case "1.3":
+ self = .v1_3
+
+ default:
+ return nil
+ }
+ }
+
var secValue: tls_protocol_version_t {
switch self {
case .v1_2:
diff --git a/WireAPI/Sources/WireAPI/Models/SelfUser/ManagingSystem.swift b/WireAPI/Sources/WireAPI/Models/SelfUser/ManagingSystem.swift
index 505e6385602..24ae2ab325b 100644
--- a/WireAPI/Sources/WireAPI/Models/SelfUser/ManagingSystem.swift
+++ b/WireAPI/Sources/WireAPI/Models/SelfUser/ManagingSystem.swift
@@ -20,7 +20,7 @@ import Foundation
/// The managing system of the self user identity
-public enum ManagingSystem {
+public enum ManagingSystem: String, Sendable {
/// User identity is managed with Wire
diff --git a/WireAPI/Sources/WireAPI/Models/SelfUser/SSOID.swift b/WireAPI/Sources/WireAPI/Models/SelfUser/SSOID.swift
index c4ac625a35e..8aba3593296 100644
--- a/WireAPI/Sources/WireAPI/Models/SelfUser/SSOID.swift
+++ b/WireAPI/Sources/WireAPI/Models/SelfUser/SSOID.swift
@@ -20,7 +20,7 @@ import Foundation
/// The sso id of the self user
-public struct SSOID: Equatable {
+public struct SSOID: Equatable, Sendable {
/// The self user's scim external id
diff --git a/WireAPI/Sources/WireAPI/Models/SelfUser/SelfUser.swift b/WireAPI/Sources/WireAPI/Models/SelfUser/SelfUser.swift
index abb730be37e..01499d4432a 100644
--- a/WireAPI/Sources/WireAPI/Models/SelfUser/SelfUser.swift
+++ b/WireAPI/Sources/WireAPI/Models/SelfUser/SelfUser.swift
@@ -20,7 +20,7 @@ import Foundation
/// User profile for self
-public struct SelfUser: Equatable {
+public struct SelfUser: Equatable, Sendable {
/// The unique id of the self user
diff --git a/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalholdInfo.swift b/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalholdInfo.swift
new file mode 100644
index 00000000000..d302f697334
--- /dev/null
+++ b/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalholdInfo.swift
@@ -0,0 +1,39 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+public typealias LegalholdPrekey = Prekey
+
+/// The team member legal hold.
+public struct TeamMemberLegalholdInfo: Equatable, Sendable {
+
+ /// The legal hold status
+
+ public let status: LegalholdStatus
+
+ /// The legal hold prekey
+
+ public let prekey: LegalholdPrekey
+
+ public init(
+ status: LegalholdStatus,
+ prekey: LegalholdPrekey
+ ) {
+ self.status = status
+ self.prekey = prekey
+ }
+}
diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationCodeUpdateEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationCodeUpdateEvent.swift
index 56917c512ce..feb85f6baf3 100644
--- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationCodeUpdateEvent.swift
+++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationCodeUpdateEvent.swift
@@ -20,7 +20,7 @@ import Foundation
/// An event where the conversation's guest link code was updated.
-public struct ConversationCodeUpdateEvent: Equatable, Codable {
+public struct ConversationCodeUpdateEvent: Equatable, Codable, Sendable {
/// The id of the conversation.
diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationEvent.swift
index ab3949715ad..b6b3fe94d87 100644
--- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationEvent.swift
+++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationEvent.swift
@@ -20,7 +20,7 @@ import Foundation
/// An event concerning conversations.
-public enum ConversationEvent: Equatable, Codable {
+public enum ConversationEvent: Equatable, Codable, Sendable {
/// A conversation's access settings were updated.
diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSMessageAddEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSMessageAddEvent.swift
index ce3aff250b8..cfe4d88ddf5 100644
--- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSMessageAddEvent.swift
+++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSMessageAddEvent.swift
@@ -20,7 +20,7 @@ import Foundation
/// An event where an mls message was received in a conversation.
-public struct ConversationMLSMessageAddEvent: Equatable, Codable {
+public struct ConversationMLSMessageAddEvent: Equatable, Codable, Sendable {
/// The id of the conversation.
diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSWelcomeEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSWelcomeEvent.swift
index fd8919a4f63..75ed4a0a214 100644
--- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSWelcomeEvent.swift
+++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSWelcomeEvent.swift
@@ -20,7 +20,7 @@ import Foundation
/// An event where an mls welcome message was received in a conversation.
-public struct ConversationMLSWelcomeEvent: Equatable, Codable {
+public struct ConversationMLSWelcomeEvent: Equatable, Codable, Sendable {
/// The id of the conversation.
diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMessageTimerUpdateEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMessageTimerUpdateEvent.swift
index 3dae5c6e6cc..20e87d8b54d 100644
--- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMessageTimerUpdateEvent.swift
+++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMessageTimerUpdateEvent.swift
@@ -20,7 +20,7 @@ import Foundation
/// An event where the message timer of a conversation was updated.
-public struct ConversationMessageTimerUpdateEvent: Equatable, Codable {
+public struct ConversationMessageTimerUpdateEvent: Equatable, Codable, Sendable {
/// The id of the conversation.
diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationProteusMessageAddEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationProteusMessageAddEvent.swift
index 7eb33c4c70d..373c87c1996 100644
--- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationProteusMessageAddEvent.swift
+++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationProteusMessageAddEvent.swift
@@ -20,7 +20,7 @@ import Foundation
/// An event where a proteus message was received in a conversation.
-public struct ConversationProteusMessageAddEvent: Equatable, Codable {
+public struct ConversationProteusMessageAddEvent: Equatable, Codable, Sendable {
/// The id of the conversation.
diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationReceiptModeUpdateEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationReceiptModeUpdateEvent.swift
index 804a79a6416..880ec2075df 100644
--- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationReceiptModeUpdateEvent.swift
+++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationReceiptModeUpdateEvent.swift
@@ -35,6 +35,6 @@ public struct ConversationReceiptModeUpdateEvent: Equatable, Codable, Sendable {
/// A value of `1` indicates read reciepts are enabled
/// and any other value indicates receipts are disabled.
- public let newRecieptMode: Int
+ public let newReceiptMode: Int
}
diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationRenameEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationRenameEvent.swift
index 9f28e590038..079dfabc869 100644
--- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationRenameEvent.swift
+++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationRenameEvent.swift
@@ -20,7 +20,7 @@ import Foundation
/// An event where the conversation's name was changed.
-public struct ConversationRenameEvent: Equatable, Codable {
+public struct ConversationRenameEvent: Equatable, Codable, Sendable {
/// The id of the conversation.
diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationTypingEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationTypingEvent.swift
index 3e7f4977b2c..18998ce4cb6 100644
--- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationTypingEvent.swift
+++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationTypingEvent.swift
@@ -20,7 +20,7 @@ import Foundation
/// An event where a user is typing in a conversation.
-public struct ConversationTypingEvent: Equatable, Codable {
+public struct ConversationTypingEvent: Equatable, Codable, Sendable {
/// The id of the conversation.
diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEvent.swift
index 66fe62bae1a..322c04b1215 100644
--- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEvent.swift
+++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEvent.swift
@@ -22,7 +22,7 @@ import Foundation
/// that can be used to incrementaly update the state of
/// the client.
-public enum UpdateEvent: Equatable, Codable {
+public enum UpdateEvent: Equatable, Codable, Sendable {
/// A conversation event.
diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEventEnvelope.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEventEnvelope.swift
index 1b89e1e5b7c..9697724a094 100644
--- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEventEnvelope.swift
+++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEventEnvelope.swift
@@ -20,7 +20,7 @@ import Foundation
/// A container for update events.
-public struct UpdateEventEnvelope: Equatable, Codable {
+public struct UpdateEventEnvelope: Equatable, Codable, Sendable {
/// The id of the event envelope.
diff --git a/WireAPI/Sources/WireAPI/Models/User/UserID.swift b/WireAPI/Sources/WireAPI/Models/User/UserID.swift
index db9cc419e44..576a237f690 100644
--- a/WireAPI/Sources/WireAPI/Models/User/UserID.swift
+++ b/WireAPI/Sources/WireAPI/Models/User/UserID.swift
@@ -18,6 +18,6 @@
import Foundation
-/// Fully quallified user identifier.
+/// Fully qualified user identifier.
public typealias UserID = QualifiedID
diff --git a/WireAPI/Sources/WireAPI/Models/UserClient/UserClientCapability.swift b/WireAPI/Sources/WireAPI/Models/UserClient/UserClientCapability.swift
index 1b5462b67b4..b28548c0c48 100644
--- a/WireAPI/Sources/WireAPI/Models/UserClient/UserClientCapability.swift
+++ b/WireAPI/Sources/WireAPI/Models/UserClient/UserClientCapability.swift
@@ -16,8 +16,6 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import Foundation
-
/// Capabilities of a user client.
public enum UserClientCapability: String, Codable, Sendable {
diff --git a/WireAPI/Sources/WireAPI/Network/APIService/APIService.swift b/WireAPI/Sources/WireAPI/Network/APIService/APIService.swift
index c59ef047635..70b0e334897 100644
--- a/WireAPI/Sources/WireAPI/Network/APIService/APIService.swift
+++ b/WireAPI/Sources/WireAPI/Network/APIService/APIService.swift
@@ -53,47 +53,7 @@ public final class APIService: APIServiceProtocol {
private let networkService: NetworkService
private let authenticationManager: any AuthenticationManagerProtocol
- /// Create a new `APIService`.
- ///
- /// - Parameters:
- /// - clientID: The id of the self client.
- /// - backendURL: The url of the target backend.
- /// - authenticationStorage: The storage for authentication objects.
- /// - minTLSVersion: The minimum supported TLS version.
-
- public convenience init(
- userID: UUID,
- clientID: String,
- backendURL: URL,
- minTLSVersion: TLSVersion,
- cookieEncryptionKey: Data,
- keychain: any KeychainProtocol
- ) {
- let configFactory = URLSessionConfigurationFactory(minTLSVersion: minTLSVersion)
- let configuration = configFactory.makeRESTAPISessionConfiguration()
- let networkService = NetworkService(baseURL: backendURL)
- let urlSession = URLSession(configuration: configuration)
- networkService.configure(with: urlSession)
-
- let cookieStorage = CookieStorage(
- userID: userID,
- cookieEncryptionKey: cookieEncryptionKey,
- keychain: keychain
- )
-
- let authenticationManager = AuthenticationManager(
- clientID: clientID,
- cookieStorage: cookieStorage,
- networkService: networkService
- )
-
- self.init(
- networkService: networkService,
- authenticationManager: authenticationManager
- )
- }
-
- init(
+ public init(
networkService: NetworkService,
authenticationManager: any AuthenticationManagerProtocol
) {
diff --git a/WireAPI/Sources/WireAPI/Network/NetworkService/NetworkService.swift b/WireAPI/Sources/WireAPI/Network/NetworkService/NetworkService.swift
index c9f8087bbec..8185ff7bbac 100644
--- a/WireAPI/Sources/WireAPI/Network/NetworkService/NetworkService.swift
+++ b/WireAPI/Sources/WireAPI/Network/NetworkService/NetworkService.swift
@@ -18,14 +18,16 @@
import Foundation
-final class NetworkService: NSObject {
+public final class NetworkService: NSObject {
private let baseURL: URL
+ private let serverTrustValidator: ServerTrustValidator
private var urlSession: URLSession?
private var webSocketsByTask = [URLSessionWebSocketTask: WebSocket]()
- init(baseURL: URL) {
+ init(baseURL: URL, serverTrustValidator: ServerTrustValidator) {
self.baseURL = baseURL
+ self.serverTrustValidator = serverTrustValidator
}
deinit {
@@ -105,13 +107,14 @@ extension NetworkService: URLSessionWebSocketDelegate {
}
-extension NetworkService: URLSessionDataDelegate {
+extension NetworkService: URLSessionTaskDelegate {
public func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: (any Error)?
) {
+ // NOTE: This method is not called when when using async/await APIs.
if let error {
print("task did complete with error: \(error)")
} else {
@@ -122,27 +125,26 @@ extension NetworkService: URLSessionDataDelegate {
public func urlSession(
_ session: URLSession,
task: URLSessionTask,
- didReceive challenge: URLAuthenticationChallenge,
- completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
- ) {
- print("task did receive challenge")
-
+ didReceive challenge:
+ URLAuthenticationChallenge
+ ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
let protectionSpace = challenge.protectionSpace
- guard protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else {
- completionHandler(.performDefaultHandling, challenge.proposedCredential)
- return
- }
-
- guard
- protectionSpace.serverTrust != nil,
- true // TODO: [WPB-10450] support certificate pinning
- else {
- completionHandler(.cancelAuthenticationChallenge, nil)
- return
+ if protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
+ guard let trust = challenge.protectionSpace.serverTrust else {
+ // If this is missing it is Apple breaking its API contract so crash.
+ fatalError("Missing server trust")
+ }
+
+ do {
+ try await serverTrustValidator.validate(trust: trust, host: protectionSpace.host)
+ return (.performDefaultHandling, challenge.proposedCredential)
+ } catch {
+ return (.cancelAuthenticationChallenge, nil)
+ }
+ } else {
+ return (.performDefaultHandling, challenge.proposedCredential)
}
-
- completionHandler(.performDefaultHandling, challenge.proposedCredential)
}
}
diff --git a/WireAPI/Sources/WireAPI/Network/PushChannel/PushChannelService.swift b/WireAPI/Sources/WireAPI/Network/PushChannel/PushChannelService.swift
index eb587850af8..6a7880ad5f4 100644
--- a/WireAPI/Sources/WireAPI/Network/PushChannel/PushChannelService.swift
+++ b/WireAPI/Sources/WireAPI/Network/PushChannel/PushChannelService.swift
@@ -35,44 +35,19 @@ public protocol PushChannelServiceProtocol {
public final class PushChannelService: PushChannelServiceProtocol {
private let networkService: NetworkService
- private let authenticationStorage: any AuthenticationStorage
-
- public convenience init(
- backendWebSocketURL: URL,
- authenticationStorage: any AuthenticationStorage,
- minTLSVersion: TLSVersion
- ) {
- let factory = URLSessionConfigurationFactory(minTLSVersion: minTLSVersion)
- let configuration = factory.makeWebSocketSessionConfiguration()
- let networkService = NetworkService(baseURL: backendWebSocketURL)
- let urlSession = URLSession(
- configuration: configuration,
- delegate: networkService,
- delegateQueue: nil
- )
- networkService.configure(with: urlSession)
-
- self.init(
- networkService: networkService,
- authenticationStorage: authenticationStorage
- )
- }
+ private let authenticationManager: any AuthenticationManagerProtocol
init(
networkService: NetworkService,
- authenticationStorage: any AuthenticationStorage
+ authenticationManager: any AuthenticationManagerProtocol
) {
self.networkService = networkService
- self.authenticationStorage = authenticationStorage
+ self.authenticationManager = authenticationManager
}
public func createPushChannel(_ request: URLRequest) async throws -> any PushChannelProtocol {
var request = request
-
- guard let accessToken = await authenticationStorage.fetchAccessToken() else {
- throw PushChannelServiceError.missingAccessToken
- }
-
+ let accessToken = try await authenticationManager.getValidAccessToken()
request.setAccessToken(accessToken)
let webSocket = try networkService.executeWebSocketRequest(request)
return PushChannel(webSocket: webSocket)
diff --git a/WireAPI/Sources/WireAPI/Network/URLSessionConfigurationFactory.swift b/WireAPI/Sources/WireAPI/Network/URLSessionConfigurationFactory.swift
index 4a56c783490..350f4814f78 100644
--- a/WireAPI/Sources/WireAPI/Network/URLSessionConfigurationFactory.swift
+++ b/WireAPI/Sources/WireAPI/Network/URLSessionConfigurationFactory.swift
@@ -21,9 +21,9 @@ import Foundation
struct URLSessionConfigurationFactory {
let minTLSVersion: TLSVersion
+ let proxySettings: ProxySettings?
func makeRESTAPISessionConfiguration() -> URLSessionConfiguration {
- // TODO: [WPB-10447] support proxy mode
let configuration = URLSessionConfiguration.ephemeral
// If no data is transmitted for this amount of time for a request, it will time out.
@@ -43,13 +43,22 @@ struct URLSessionConfigurationFactory {
configuration.tlsMinimumSupportedProtocolVersion = minTLSVersion.secValue
configuration.urlCache = nil
+
+ if let proxySettings {
+ configuration.connectionProxyDictionary = proxySettings.proxyDictionary()
+ }
+
return configuration
}
func makeWebSocketSessionConfiguration() -> URLSessionConfiguration {
- // TODO: [WPB-10447] support proxy mode
let configuration = URLSessionConfiguration.ephemeral
configuration.tlsMinimumSupportedProtocolVersion = minTLSVersion.secValue
+
+ if let proxySettings {
+ configuration.connectionProxyDictionary = proxySettings.proxyDictionary()
+ }
+
return configuration
}
diff --git a/WireAPI/Sources/WireAPISupport/Sourcery/sourcery.yml b/WireAPI/Sources/WireAPISupport/Sourcery/sourcery.yml
index 6a04a2891cb..5f43a95fd38 100644
--- a/WireAPI/Sources/WireAPISupport/Sourcery/sourcery.yml
+++ b/WireAPI/Sources/WireAPISupport/Sourcery/sourcery.yml
@@ -3,7 +3,7 @@ sources:
templates:
- ${TARGET_DIR}/Sourcery/AutoMockable.stencil
output:
- ${DERIVED_SOURCES_DIR}
+ ${DERIVED_SOURCES_DIR}
args:
- autoMockableImports: []
- autoMockableTestableImports: ["WireAPI"]
+ autoMockablePublicImports: ["Foundation"]
+ autoMockableTestableImports: ["WireAPI"]
diff --git a/WireAPI/.swiftpm/WireAPI.xctestplan b/WireAPI/Tests/TestPlans/AllTests.xctestplan
similarity index 62%
rename from WireAPI/.swiftpm/WireAPI.xctestplan
rename to WireAPI/Tests/TestPlans/AllTests.xctestplan
index cc3539a4c0c..e8951419670 100644
--- a/WireAPI/.swiftpm/WireAPI.xctestplan
+++ b/WireAPI/Tests/TestPlans/AllTests.xctestplan
@@ -1,20 +1,22 @@
{
"configurations" : [
{
- "id" : "CA909C45-1D66-4C47-BBFD-53CA3AB99A82",
- "name" : "Test Scheme Action",
+ "id" : "FDB58F5E-9DC3-4935-B919-AA295F684D19",
+ "name" : "Configuration 1",
"options" : {
}
}
],
"defaultOptions" : {
+ "language" : "en",
+ "region" : "DE",
"testExecutionOrdering" : "random"
},
"testTargets" : [
{
"target" : {
- "containerPath" : "container:",
+ "containerPath" : "container:WireAPI",
"identifier" : "WireAPITests",
"name" : "WireAPITests"
}
diff --git a/WireAPI/Tests/WireAPITests/APIs/BackendInfoAPI/BackendInfoAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/BackendInfoAPI/BackendInfoAPITests.swift
index d988080d621..28c12f0906a 100644
--- a/WireAPI/Tests/WireAPITests/APIs/BackendInfoAPI/BackendInfoAPITests.swift
+++ b/WireAPI/Tests/WireAPITests/APIs/BackendInfoAPI/BackendInfoAPITests.swift
@@ -70,6 +70,7 @@ final class BackendInfoAPITests: XCTestCase {
BackendInfo(
domain: "example.com",
isFederationEnabled: true,
+ isMLSEnabled: false,
supportedVersions: [.v0, .v1, .v2],
developmentVersions: []
)
@@ -97,6 +98,7 @@ final class BackendInfoAPITests: XCTestCase {
BackendInfo(
domain: "example.com",
isFederationEnabled: true,
+ isMLSEnabled: false,
supportedVersions: [.v0, .v1, .v2],
developmentVersions: [.v3]
)
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift
index 9b3b7f67d4f..246d0d79575 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift
@@ -19,29 +19,24 @@
import WireTestingPackage
import XCTest
@testable import WireAPI
+@testable import WireAPISupport
final class ConversationsAPITests: XCTestCase {
- private var httpRequestSnapshotHelper: HTTPRequestSnapshotHelper!
- private var apiSnapshotHelper: APISnapshotHelper!
+ private var apiSnapshotHelper: APIServiceSnapshotHelper!
// MARK: - Setup
override func setUp() {
- super.setUp()
+ apiSnapshotHelper = APIServiceSnapshotHelper { apiService, apiVersion in
+ ConversationsAPIBuilder(apiService: apiService)
+ .makeAPI(for: apiVersion)
- httpRequestSnapshotHelper = HTTPRequestSnapshotHelper()
- apiSnapshotHelper = APISnapshotHelper { httpClient, apiVersion in
- let builder = ConversationsAPIBuilder(httpClient: httpClient)
- return builder.makeAPI(for: apiVersion)
}
}
override func tearDown() {
apiSnapshotHelper = nil
- httpRequestSnapshotHelper = nil
-
- super.tearDown()
}
// MARK: - Tests
@@ -63,21 +58,6 @@ final class ConversationsAPITests: XCTestCase {
}
}
- func testGetConversationIdentifiers() async throws {
- // given
- let apiVersions = Set(APIVersion.allCases).subtracting([.v0])
-
- // when
- // then
- try await apiSnapshotHelper.verifyRequest(for: apiVersions) { sut in
- let pager = try await sut.getConversationIdentifiers()
-
- for try await _ in pager {
- // trigger fetching data
- }
- }
- }
-
func testGetMLSOneToOneConversationRequest() async throws {
// Given
@@ -94,18 +74,30 @@ final class ConversationsAPITests: XCTestCase {
}
}
+ func testGetConversationIdentifiers() async throws {
+ // given
+ let apiVersions = Set(APIVersion.allCases).subtracting([.v0])
+
+ // when
+ // then
+ try await apiSnapshotHelper.verifyRequest(for: apiVersions) { sut in
+ let pager = try await sut.getConversationIdentifiers()
+
+ for try await _ in pager {
+ // trigger fetching data
+ }
+ }
+ }
+
func testGetLegacyConversationIdentifiers_givenV0AndSuccessResponse200_thenVerifyRequests() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockJSONResource(
- code: .ok,
- name: "testGetLegacyConversationIdentifiers_givenV0AndSuccessResponse200"
- )
- ]
+
+ let apiService = MockAPIServiceProtocol.withResponses([
+ (.ok, "testGetLegacyConversationIdentifiers_givenV0AndSuccessResponse200")
+ ])
// when
- let api = ConversationsAPIV0(httpClient: httpClient)
+ let api = ConversationsAPIV0(apiService: apiService)
let pager = try await api.getLegacyConversationIdentifiers()
for try await _ in pager {
@@ -113,26 +105,23 @@ final class ConversationsAPITests: XCTestCase {
}
// then
- for (index, request) in httpClient.receivedRequests.enumerated() {
- await httpRequestSnapshotHelper.verifyRequest(request: request, resourceName: "v0.\(index)")
+
+ try await apiSnapshotHelper.verifyRequest(for: [.v0], apiService: apiService) { api in
+ _ = try await api.getLegacyConversationIdentifiers()
}
}
func testGetLegacyConversationIdentifiers_givenV0AndSuccessResponse200_thenVerifyResponse() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockJSONResource(
- code: .ok,
- name: "testGetLegacyConversationIdentifiers_givenV0AndSuccessResponse200"
- )
- ]
+ let apiService = MockAPIServiceProtocol.withResponses([
+ (.ok, "testGetLegacyConversationIdentifiers_givenV0AndSuccessResponse200")
+ ])
let expectedIDs: [UUID] = [
try XCTUnwrap(UUID(uuidString: "14c3f0ff-1a46-4e66-8845-ae084f09c483"))
]
- let api = ConversationsAPIV0(httpClient: httpClient)
+ let api = ConversationsAPIV0(apiService: apiService)
// when
let pager = try await api.getLegacyConversationIdentifiers()
@@ -146,12 +135,13 @@ final class ConversationsAPITests: XCTestCase {
func testGetLegacyConversationIdentifiers_givenV0AndErrorResponse() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockError(code: .serviceUnavailable, label: "service unavailable")
- ]
- let api = ConversationsAPIV0(httpClient: httpClient)
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .serviceUnavailable,
+ label: "service unavailable"
+ )
+
+ let api = ConversationsAPIV0(apiService: apiService)
// when
// then
@@ -169,37 +159,28 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversationIdentifiers_givenV1AndSuccessResponse200_thenVerifyRequests() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockJSONResource(
- code: .ok,
- name: "testGetConversationIdentifiers_givenV1AndSuccessResponse200"
- )
- ]
+ let apiService = MockAPIServiceProtocol.withResponses([
+ (.ok, "testGetConversationIdentifiers_givenV1AndSuccessResponse200")
+ ])
// when
- let api = ConversationsAPIV1(httpClient: httpClient)
+ let api = ConversationsAPIV1(apiService: apiService)
let pager = try await api.getConversationIdentifiers()
for try await _ in pager {
// trigger fetching date
}
- // then
- for (index, request) in httpClient.receivedRequests.enumerated() {
- await httpRequestSnapshotHelper.verifyRequest(request: request, resourceName: "v1.\(index)")
+ try await apiSnapshotHelper.verifyRequest(for: [.v1], apiService: apiService) { api in
+ _ = try await api.getConversationIdentifiers()
}
}
func testGetConversationIdentifiers_givenV1AndSuccessResponse200_thenVerifyResponse() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockJSONResource(
- code: .ok,
- name: "testGetConversationIdentifiers_givenV1AndSuccessResponse200"
- )
- ]
+ let apiService = MockAPIServiceProtocol.withResponses([
+ (.ok, "testGetConversationIdentifiers_givenV1AndSuccessResponse200")
+ ])
let expectedIDs: [QualifiedID] = [
QualifiedID(
@@ -208,7 +189,7 @@ final class ConversationsAPITests: XCTestCase {
)
]
- let api = ConversationsAPIV1(httpClient: httpClient)
+ let api = ConversationsAPIV1(apiService: apiService)
// when
let pager = try await api.getConversationIdentifiers()
@@ -222,12 +203,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversationIdentifiers_givenV1AndErrorResponse() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockError(code: .serviceUnavailable, label: "service unavailable")
- ]
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .serviceUnavailable,
+ label: "service unavailable"
+ )
- let api = ConversationsAPIV1(httpClient: httpClient)
+ let api = ConversationsAPIV1(apiService: apiService)
// when
// then
@@ -261,15 +242,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversations_givenV0AndSuccessResponse200_thenVerifyResponse() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockJSONResource(
- code: .ok,
- name: "testGetConversations_givenV0AndSuccessResponse200"
- )
- ]
- let api = ConversationsAPIV0(httpClient: httpClient)
+ let apiService = MockAPIServiceProtocol.withResponses([
+ (.ok, "testGetConversations_givenV0AndSuccessResponse200")
+ ])
+
+ let api = ConversationsAPIV0(apiService: apiService)
// when
// then
@@ -281,12 +259,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversations_givenV0AndSuccessResponse400() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockError(code: .badRequest, label: "invalid body")
- ]
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .badRequest,
+ label: "invalid body"
+ )
- let api = ConversationsAPIV0(httpClient: httpClient)
+ let api = ConversationsAPIV0(apiService: apiService)
// when
// then
@@ -302,12 +280,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversations_givenV0AndSuccessResponse503() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockError(code: .serviceUnavailable, label: "service unavailable")
- ]
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .serviceUnavailable,
+ label: "service unavailable"
+ )
- let api = ConversationsAPIV0(httpClient: httpClient)
+ let api = ConversationsAPIV0(apiService: apiService)
// when
// then
@@ -323,15 +301,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversations_givenV2AndSuccessResponse200_thenVerifyResponse() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockJSONResource(
- code: .ok,
- name: "testGetConversations_givenV2AndSuccessResponse200"
- )
- ]
- let api = ConversationsAPIV2(httpClient: httpClient)
+ let apiService = MockAPIServiceProtocol.withResponses([
+ (.ok, "testGetConversations_givenV2AndSuccessResponse200")
+ ])
+
+ let api = ConversationsAPIV2(apiService: apiService)
// when
// then
@@ -343,12 +318,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversations_givenV2AndSuccessResponse400() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockError(code: .badRequest, label: "invalid body")
- ]
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .badRequest,
+ label: "invalid body"
+ )
- let api = ConversationsAPIV2(httpClient: httpClient)
+ let api = ConversationsAPIV2(apiService: apiService)
// when
// then
@@ -364,12 +339,13 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversations_givenV2AndSuccessResponse503() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockError(code: .serviceUnavailable, label: "service unavailable")
- ]
- let api = ConversationsAPIV2(httpClient: httpClient)
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .serviceUnavailable,
+ label: "service unavailable"
+ )
+
+ let api = ConversationsAPIV2(apiService: apiService)
// when
// then
@@ -385,15 +361,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversations_givenV3AndSuccessResponse200_thenVerifyResponse() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockJSONResource(
- code: .ok,
- name: "testGetConversations_givenV3AndSuccessResponse200"
- )
- ]
- let api = ConversationsAPIV3(httpClient: httpClient)
+ let apiService = MockAPIServiceProtocol.withResponses([
+ (.ok, "testGetConversations_givenV3AndSuccessResponse200")
+ ])
+
+ let api = ConversationsAPIV3(apiService: apiService)
// when
// then
@@ -409,12 +382,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversations_givenV3AndSuccessResponse400() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockError(code: .badRequest, label: "invalid body")
- ]
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .badRequest,
+ label: "invalid body"
+ )
- let api = ConversationsAPIV3(httpClient: httpClient)
+ let api = ConversationsAPIV3(apiService: apiService)
// when
// then
@@ -430,12 +403,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversations_givenV3AndSuccessResponse503() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockError(code: .serviceUnavailable, label: "service unavailable")
- ]
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .serviceUnavailable,
+ label: "service unavailable"
+ )
- let api = ConversationsAPIV3(httpClient: httpClient)
+ let api = ConversationsAPIV3(apiService: apiService)
// when
// then
@@ -451,15 +424,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversations_givenV5AndSuccessResponse200_thenVerifyResponse() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockJSONResource(
- code: .ok,
- name: "testGetConversations_givenV5AndSuccessResponse200"
- )
- ]
- let api = ConversationsAPIV5(httpClient: httpClient)
+ let apiService = MockAPIServiceProtocol.withResponses([
+ (.ok, "testGetConversations_givenV5AndSuccessResponse200")
+ ])
+
+ let api = ConversationsAPIV5(apiService: apiService)
// when
// then
@@ -475,12 +445,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetConversations_givenV5AndSuccessResponse503() async throws {
// given
- let httpClient = MockHTTPResponsesClient()
- httpClient.httpResponses = [
- try HTTPResponse.mockError(code: .serviceUnavailable, label: "service unavailable")
- ]
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .serviceUnavailable,
+ label: "service unavailable"
+ )
- let api = ConversationsAPIV5(httpClient: httpClient)
+ let api = ConversationsAPIV5(apiService: apiService)
// when
// then
@@ -497,43 +467,38 @@ final class ConversationsAPITests: XCTestCase {
func testGetMLSOneToOneConversation_Success_Response_V5_And_Next_Versions() async throws {
// Given
- let httpClient = try HTTPClientMock(
- code: .ok,
- payloadResourceName: "testGetMLSOneOnOneConversationV5SuccessResponse200"
+ let supportedVersions = APIVersion.v5.andNextVersions
+
+ let mocks: [MockAPIServiceProtocol.Response] = Array(
+ repeating: (.ok, "testGetMLSOneOnOneConversationV5SuccessResponse200"),
+ count: supportedVersions.count
)
- let supportedVersions = APIVersion.v5.andNextVersions
+ let apiService = MockAPIServiceProtocol.withResponses(mocks)
- let suts = supportedVersions.map { $0.buildAPI(client: httpClient) }
+ let suts = supportedVersions.map { $0.buildAPI(apiService: apiService) }
// When
- try await withThrowingTaskGroup(of: Conversation.self) { taskGroup in
- for sut in suts {
- taskGroup.addTask {
- try await sut.getMLSOneToOneConversation(
- userID: Scaffolding.userID,
- in: Scaffolding.domain
- )
- }
- }
+ for sut in suts {
+ let mlsConversation = try await sut.getMLSOneToOneConversation(
+ userID: Scaffolding.userID,
+ in: Scaffolding.domain
+ )
- for try await value in taskGroup {
- // Then
- XCTAssertEqual(value.id, Scaffolding.mlsConversationID)
- }
+ XCTAssertEqual(mlsConversation.id, Scaffolding.mlsConversationID)
}
}
func testGetMLSOneToOneConversation_UnsupportedVersionError_V0_to_V4() async throws {
// Given
- let httpClient = HTTPClientMock(
- code: .ok,
- payload: nil
- )
+
+ let apiService = MockAPIServiceProtocol.withResponses([
+ (.ok, "")
+ ])
let unsupportedVersions: [APIVersion] = [.v0, .v1, .v2, .v3, .v4]
- let suts = unsupportedVersions.map { $0.buildAPI(client: httpClient) }
+ let suts = unsupportedVersions.map { $0.buildAPI(apiService: apiService) }
try await withThrowingTaskGroup(of: Void.self) { taskGroup in
for sut in suts {
@@ -556,12 +521,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetMLSOneToOneConversation_Failure_Response_MLS_Not_Enabled() async throws {
// Given
- let httpClient = try HTTPClientMock(
- code: .badRequest,
- errorLabel: "mls-not-enabled"
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .badRequest,
+ label: "mls-not-enabled"
)
- let sut = APIVersion.v5.buildAPI(client: httpClient)
+ let sut = APIVersion.v5.buildAPI(apiService: apiService)
// Then
@@ -577,12 +542,12 @@ final class ConversationsAPITests: XCTestCase {
func testGetMLSOneToOneConversation_Failure_Response_Not_Connected() async throws {
// Given
- let httpClient = try HTTPClientMock(
- code: .forbidden,
- errorLabel: "not-connected"
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .forbidden,
+ label: "not-connected"
)
- let sut = APIVersion.v5.buildAPI(client: httpClient)
+ let sut = APIVersion.v5.buildAPI(apiService: apiService)
// Then
@@ -598,12 +563,11 @@ final class ConversationsAPITests: XCTestCase {
func testGetMLSOneToOneConversation_Failure_UserID_And_Domain_Empty() async throws {
// Given
- let httpClient = try HTTPClientMock(
- code: .ok,
- payloadResourceName: "testGetMLSOneOnOneConversationV5SuccessResponse200"
- )
+ let apiService = MockAPIServiceProtocol.withResponses([
+ (.ok, "testGetMLSOneOnOneConversationV5SuccessResponse200")
+ ])
- let sut = APIVersion.v5.buildAPI(client: httpClient)
+ let sut = APIVersion.v5.buildAPI(apiService: apiService)
// Then
@@ -616,17 +580,205 @@ final class ConversationsAPITests: XCTestCase {
}
}
+ func testGetConversationGuestLink() async throws {
+ // Given
+
+ let apiVersions = APIVersion.allCases
+ let conversationID = Scaffolding.conversationID.uuidString
+
+ // Then
+
+ try await apiSnapshotHelper.verifyRequest(for: apiVersions) { sut in
+ // When
+ _ = try await sut.getConversationGuestLink(
+ conversationID: conversationID
+ )
+ }
+ }
+
+ func testGetConversationGuestLink_Success_Response_V0_To_V3() async throws {
+
+ let supportedVersions: [APIVersion] = [.v0, .v1, .v2, .v3]
+
+ let mocks: [MockAPIServiceProtocol.Response] = Array(
+ repeating: (.ok, "testGetConversationGuestLinkV0SuccessResponse200"),
+ count: supportedVersions.count
+ )
+
+ let apiService = MockAPIServiceProtocol.withResponses(mocks)
+
+ let conversationID = Scaffolding.conversationID.uuidString
+ let suts = supportedVersions.map { $0.buildAPI(apiService: apiService) }
+
+ // When
+
+ for sut in suts {
+ let uri = try await sut.getConversationGuestLink(conversationID: conversationID)
+
+ XCTAssertEqual(uri, Scaffolding.guestLinkV0)
+ }
+
+ }
+
+ func testGetConversationGuestLink_Success_Response_V4_And_Next_Versions() async throws {
+
+ let supportedVersions = APIVersion.v4.andNextVersions
+
+ let mocks: [MockAPIServiceProtocol.Response] = Array(
+ repeating: (.ok, "testGetConversationGuestLinkV4SuccessResponse200"),
+ count: supportedVersions.count
+ )
+
+ let apiService = MockAPIServiceProtocol.withResponses(mocks)
+
+ let conversationID = Scaffolding.conversationID.uuidString
+ let suts = supportedVersions.map { $0.buildAPI(apiService: apiService) }
+
+ // When
+
+ for sut in suts {
+ let uri = try await sut.getConversationGuestLink(conversationID: conversationID)
+
+ XCTAssertEqual(uri, Scaffolding.guestLinkV4)
+ }
+
+ }
+
+ func testGetConversationGuestLinkV0_Failure_Access_Denied() async throws {
+ // Given
+
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .forbidden,
+ label: "access-denied"
+ )
+
+ let conversationID = Scaffolding.conversationID.uuidString
+
+ let sut = APIVersion.v0.buildAPI(apiService: apiService)
+
+ // Then
+
+ await XCTAssertThrowsErrorAsync(ConversationsAPIError.accessDenied) {
+ // When
+ try await sut.getConversationGuestLink(conversationID: conversationID)
+ }
+ }
+
+ func testGetConversationGuestLinkV0_Failure_Invalid_Conversation_ID() async throws {
+ // Given
+
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .notFound,
+ label: "cnv"
+ )
+
+ let conversationID = Scaffolding.conversationID.uuidString
+
+ let sut = APIVersion.v0.buildAPI(apiService: apiService)
+
+ // Then
+
+ await XCTAssertThrowsErrorAsync(ConversationsAPIError.invalidConversationID) {
+ // When
+ try await sut.getConversationGuestLink(conversationID: conversationID)
+ }
+ }
+
+ func testGetConversationGuestLinkV0_Failure_No_Conversation_Found() async throws {
+ // Given
+
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .notFound,
+ label: "no-conversation"
+ )
+
+ let conversationID = Scaffolding.conversationID.uuidString
+
+ let sut = APIVersion.v0.buildAPI(apiService: apiService)
+
+ // Then
+
+ await XCTAssertThrowsErrorAsync(ConversationsAPIError.conversationNotFound) {
+ // When
+ try await sut.getConversationGuestLink(conversationID: conversationID)
+ }
+ }
+
+ func testGetConversationGuestLinkV0_Failure_No_Conversation_Code_Found() async throws {
+ // Given
+
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .notFound,
+ label: "no-conversation-code"
+ )
+
+ let conversationID = Scaffolding.conversationID.uuidString
+
+ let sut = APIVersion.v0.buildAPI(apiService: apiService)
+
+ // Then
+
+ await XCTAssertThrowsErrorAsync(ConversationsAPIError.conversationCodeNotFound) {
+ // When
+ try await sut.getConversationGuestLink(conversationID: conversationID)
+ }
+ }
+
+ func testGetConversationGuestLinkV0_Failure_Guest_Links_Disabled() async throws {
+ // Given
+
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .conflict,
+ label: "guest-links-disabled"
+ )
+
+ let conversationID = Scaffolding.conversationID.uuidString
+
+ let sut = APIVersion.v0.buildAPI(apiService: apiService)
+
+ // Then
+
+ await XCTAssertThrowsErrorAsync(ConversationsAPIError.guestLinksDisabled) {
+ // When
+ try await sut.getConversationGuestLink(conversationID: conversationID)
+ }
+ }
+
+ func testGetConversationGuestLinkV4_Invalid_Conversation_ID() async throws {
+ // Given
+
+ // Dedicated error code in V4
+ let apiService = MockAPIServiceProtocol.withError(
+ statusCode: .badRequest,
+ label: "cnv"
+ )
+
+ let conversationID = Scaffolding.conversationID.uuidString
+
+ let sut = APIVersion.v4.buildAPI(apiService: apiService)
+
+ // Then
+
+ await XCTAssertThrowsErrorAsync(ConversationsAPIError.invalidConversationID) {
+ // When
+ try await sut.getConversationGuestLink(conversationID: conversationID)
+ }
+ }
+
private enum Scaffolding {
static let userID = "99db9768-04e3-4b5d-9268-831b6a25c4ab"
static let domain = "domain.com"
static let mlsConversationID = UUID(uuidString: "99db9768-04e3-4b5d-9268-831b6a25c4ab")!
+ static let conversationID = UUID.mockID1
+ static let guestLinkV0 = "https://exampleV0.com"
+ static let guestLinkV4 = "https://exampleV4.com"
}
}
private extension APIVersion {
- func buildAPI(client: any HTTPClient) -> any ConversationsAPI {
- let builder = ConversationsAPIBuilder(httpClient: client)
+ func buildAPI(apiService: any APIServiceProtocol) -> any ConversationsAPI {
+ let builder = ConversationsAPIBuilder(apiService: apiService)
return builder.makeAPI(for: self)
}
}
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/Resources/testGetConversationGuestLinkV0SuccessResponse200.json b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/Resources/testGetConversationGuestLinkV0SuccessResponse200.json
new file mode 100644
index 00000000000..36d1dfee30d
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/Resources/testGetConversationGuestLinkV0SuccessResponse200.json
@@ -0,0 +1,5 @@
+{
+ "code": "aGVsbG8",
+ "key": "aGVsbG8",
+ "uri": "https://exampleV0.com"
+}
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/Resources/testGetConversationGuestLinkV4SuccessResponse200.json b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/Resources/testGetConversationGuestLinkV4SuccessResponse200.json
new file mode 100644
index 00000000000..49f1cd04fed
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/Resources/testGetConversationGuestLinkV4SuccessResponse200.json
@@ -0,0 +1,6 @@
+{
+ "code": "stringstringstringst",
+ "has_password": true,
+ "key": "stringstringstringst",
+ "uri": "https://exampleV4.com"
+}
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v0.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v0.txt
new file mode 100644
index 00000000000..dab05b25a68
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v0.txt
@@ -0,0 +1,2 @@
+curl \
+ "/conversations/213248A1-5499-418F-8173-5010D1C1E506/code"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v1.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v1.txt
new file mode 100644
index 00000000000..a7e02a85898
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v1.txt
@@ -0,0 +1,2 @@
+curl \
+ "/v1/conversations/213248A1-5499-418F-8173-5010D1C1E506/code"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v2.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v2.txt
new file mode 100644
index 00000000000..f46d99c4e09
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v2.txt
@@ -0,0 +1,2 @@
+curl \
+ "/v2/conversations/213248A1-5499-418F-8173-5010D1C1E506/code"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v3.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v3.txt
new file mode 100644
index 00000000000..69be2f98858
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v3.txt
@@ -0,0 +1,2 @@
+curl \
+ "/v3/conversations/213248A1-5499-418F-8173-5010D1C1E506/code"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v4.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v4.txt
new file mode 100644
index 00000000000..dfd0c5df539
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v4.txt
@@ -0,0 +1,2 @@
+curl \
+ "/v4/conversations/213248A1-5499-418F-8173-5010D1C1E506/code"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v5.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v5.txt
new file mode 100644
index 00000000000..7ddc0969688
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v5.txt
@@ -0,0 +1,2 @@
+curl \
+ "/v5/conversations/213248A1-5499-418F-8173-5010D1C1E506/code"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v6.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v6.txt
new file mode 100644
index 00000000000..bfbc0d59833
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationGuestLink.request-0-v6.txt
@@ -0,0 +1,2 @@
+curl \
+ "/v6/conversations/213248A1-5499-418F-8173-5010D1C1E506/code"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v1.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v1.txt
index 4a1e94b1d7f..353853b8a98 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v1.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v1.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/v1/conversations/list-ids/"
- - method: post
- - body: {"size":500}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"size\":500}" \
+ "/v1/conversations/list-ids/"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v2.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v2.txt
index ce9cbc1ab00..26f328ac638 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v2.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v2.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/v2/conversations/list-ids/"
- - method: post
- - body: {"size":500}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"size\":500}" \
+ "/v2/conversations/list-ids/"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v3.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v3.txt
index 1a79004b08d..fb557932e8e 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v3.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v3.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/v3/conversations/list-ids/"
- - method: post
- - body: {"size":500}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"size\":500}" \
+ "/v3/conversations/list-ids/"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v4.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v4.txt
index 4feb72c1b45..3213151b6ad 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v4.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v4.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/v4/conversations/list-ids/"
- - method: post
- - body: {"size":500}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"size\":500}" \
+ "/v4/conversations/list-ids/"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v5.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v5.txt
index 7e6844fa519..ff6c7bb768b 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v5.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v5.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/v5/conversations/list-ids/"
- - method: post
- - body: {"size":500}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"size\":500}" \
+ "/v5/conversations/list-ids/"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v6.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v6.txt
index c6634bbe40d..535e14a4577 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v6.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers.request-0-v6.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/v6/conversations/list-ids/"
- - method: post
- - body: {"size":500}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"size\":500}" \
+ "/v6/conversations/list-ids/"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers_givenV1AndSuccessResponse200_thenVerifyRequests.request-0-v1.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers_givenV1AndSuccessResponse200_thenVerifyRequests.request-0-v1.txt
new file mode 100644
index 00000000000..353853b8a98
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers_givenV1AndSuccessResponse200_thenVerifyRequests.request-0-v1.txt
@@ -0,0 +1,5 @@
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"size\":500}" \
+ "/v1/conversations/list-ids/"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers_givenV1AndSuccessResponse200_thenVerifyRequests.v1-0.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers_givenV1AndSuccessResponse200_thenVerifyRequests.v1-0.txt
deleted file mode 100644
index 4a1e94b1d7f..00000000000
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversationIdentifiers_givenV1AndSuccessResponse200_thenVerifyRequests.v1-0.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-- HTTPRequest
- - path: "/v1/conversations/list-ids/"
- - method: post
- - body: {"size":500}
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v0.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v0.txt
index 973422a6b95..fb139cd66ba 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v0.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v0.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/conversations/list/v2"
- - method: post
- - body: {"qualified_ids":[{"domain":"wire.com","id":"213248A1-5499-418F-8173-5010D1C1E506"}]}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"qualified_ids\":[{\"domain\":\"wire.com\",\"id\":\"213248A1-5499-418F-8173-5010D1C1E506\"}]}" \
+ "/conversations/list/v2"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v1.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v1.txt
index eb807b4ee02..59adca7974f 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v1.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v1.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/v1/conversations/list/v2"
- - method: post
- - body: {"qualified_ids":[{"domain":"wire.com","id":"213248A1-5499-418F-8173-5010D1C1E506"}]}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"qualified_ids\":[{\"domain\":\"wire.com\",\"id\":\"213248A1-5499-418F-8173-5010D1C1E506\"}]}" \
+ "/v1/conversations/list/v2"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v2.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v2.txt
index efef9c29192..3edfa87f1d0 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v2.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v2.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/v2/conversations/list"
- - method: post
- - body: {"qualified_ids":[{"domain":"wire.com","id":"213248A1-5499-418F-8173-5010D1C1E506"}]}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"qualified_ids\":[{\"domain\":\"wire.com\",\"id\":\"213248A1-5499-418F-8173-5010D1C1E506\"}]}" \
+ "/v2/conversations/list"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v3.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v3.txt
index f8b4d21934f..ce623e2c087 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v3.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v3.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/v3/conversations/list"
- - method: post
- - body: {"qualified_ids":[{"domain":"wire.com","id":"213248A1-5499-418F-8173-5010D1C1E506"}]}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"qualified_ids\":[{\"domain\":\"wire.com\",\"id\":\"213248A1-5499-418F-8173-5010D1C1E506\"}]}" \
+ "/v3/conversations/list"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v4.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v4.txt
index 4cc4e4c9a03..196705eb19f 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v4.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v4.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/v4/conversations/list"
- - method: post
- - body: {"qualified_ids":[{"domain":"wire.com","id":"213248A1-5499-418F-8173-5010D1C1E506"}]}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"qualified_ids\":[{\"domain\":\"wire.com\",\"id\":\"213248A1-5499-418F-8173-5010D1C1E506\"}]}" \
+ "/v4/conversations/list"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v5.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v5.txt
index d62f398138a..b937777d2ab 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v5.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v5.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/v5/conversations/list"
- - method: post
- - body: {"qualified_ids":[{"domain":"wire.com","id":"213248A1-5499-418F-8173-5010D1C1E506"}]}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"qualified_ids\":[{\"domain\":\"wire.com\",\"id\":\"213248A1-5499-418F-8173-5010D1C1E506\"}]}" \
+ "/v5/conversations/list"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v6.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v6.txt
index c770cdf5dcb..39969324dc8 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v6.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetConversations_givenAllAPIVersions_thenVerifyRequests.request-0-v6.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/v6/conversations/list"
- - method: post
- - body: {"qualified_ids":[{"domain":"wire.com","id":"213248A1-5499-418F-8173-5010D1C1E506"}]}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"qualified_ids\":[{\"domain\":\"wire.com\",\"id\":\"213248A1-5499-418F-8173-5010D1C1E506\"}]}" \
+ "/v6/conversations/list"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetLegacyConversationIdentifiers.request-0-v0.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetLegacyConversationIdentifiers.request-0-v0.txt
index 28b0bcc81c1..e2a62efa409 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetLegacyConversationIdentifiers.request-0-v0.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetLegacyConversationIdentifiers.request-0-v0.txt
@@ -1,4 +1,5 @@
-- HTTPRequest
- - path: "/conversations/list-ids/"
- - method: post
- - body: {"size":500}
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"size\":500}" \
+ "/conversations/list-ids/"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetLegacyConversationIdentifiers_givenV0AndSuccessResponse200_thenVerifyRequests.request-0-v0.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetLegacyConversationIdentifiers_givenV0AndSuccessResponse200_thenVerifyRequests.request-0-v0.txt
new file mode 100644
index 00000000000..e2a62efa409
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetLegacyConversationIdentifiers_givenV0AndSuccessResponse200_thenVerifyRequests.request-0-v0.txt
@@ -0,0 +1,5 @@
+curl \
+ --request POST \
+ --header "Content-Type: application/json" \
+ --data "{\"size\":500}" \
+ "/conversations/list-ids/"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetLegacyConversationIdentifiers_givenV0AndSuccessResponse200_thenVerifyRequests.v0-0.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetLegacyConversationIdentifiers_givenV0AndSuccessResponse200_thenVerifyRequests.v0-0.txt
deleted file mode 100644
index 28b0bcc81c1..00000000000
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetLegacyConversationIdentifiers_givenV0AndSuccessResponse200_thenVerifyRequests.v0-0.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-- HTTPRequest
- - path: "/conversations/list-ids/"
- - method: post
- - body: {"size":500}
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v5.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v5.txt
index db3bc27a409..87649538310 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v5.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v5.txt
@@ -1,4 +1,2 @@
-- HTTPRequest
- - path: "/v5/conversations/one2one/domain.com/99db9768-04e3-4b5d-9268-831b6a25c4ab"
- - method: get
- - body: none
+curl \
+ "/v5/conversations/one2one/domain.com/99db9768-04e3-4b5d-9268-831b6a25c4ab"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v6.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v6.txt
index 8fbe78be92b..1aea3e5d2ac 100644
--- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v6.txt
+++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v6.txt
@@ -1,4 +1,2 @@
-- HTTPRequest
- - path: "/v6/conversations/one2one/domain.com/99db9768-04e3-4b5d-9268-831b6a25c4ab"
- - method: get
- - body: none
+curl \
+ "/v6/conversations/one2one/domain.com/99db9768-04e3-4b5d-9268-831b6a25c4ab"
\ No newline at end of file
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift
index 41552dab264..0517d6cfea3 100644
--- a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift
@@ -59,9 +59,9 @@ final class TeamsAPITests: XCTestCase {
}
}
- func testGetLegalholdStatusRequest() async throws {
+ func testGetLegalholdInfoRequest() async throws {
try await apiSnapshotHelper.verifyRequestForAllAPIVersions { sut in
- _ = try await sut.getLegalholdStatus(for: .mockID1, userID: .mockID2)
+ _ = try await sut.getLegalholdInfo(for: .mockID1, userID: .mockID2)
}
}
@@ -252,13 +252,17 @@ final class TeamsAPITests: XCTestCase {
}
}
- func testGetLegalholdStatus_SuccessResponse_200_V0() async throws {
+ func testGetLegalholdInfo_SuccessResponse_200_V0() async throws {
// Given
let httpClient = try HTTPClientMock(
code: .ok,
jsonResponse: """
{
- "status": "pending"
+ "status": "pending",
+ "last_prekey": {
+ "id": 12345,
+ "key": "foo"
+ }
}
"""
)
@@ -266,43 +270,33 @@ final class TeamsAPITests: XCTestCase {
let sut = TeamsAPIV0(httpClient: httpClient)
// When
- let result = try await sut.getLegalholdStatus(
+ let result = try await sut.getLegalholdInfo(
for: Team.ID(),
userID: UUID()
)
// Then
- XCTAssertEqual(result, .pending)
+ let expectedPrekey = LegalholdPrekey(id: 12_345, base64EncodedKey: "foo")
+ XCTAssertEqual(result.status, .pending)
+ XCTAssertEqual(result.prekey, expectedPrekey)
}
- func testGetLegalholdStatus_FailureResponse_InvalidRequest_V0() async throws {
- // Given
- let httpClient = try HTTPClientMock(code: .notFound, errorLabel: "")
- let sut = TeamsAPIV0(httpClient: httpClient)
-
- // Then
- await XCTAssertThrowsErrorAsync(TeamsAPIError.invalidRequest) {
- // When
- try await sut.getLegalholdStatus(
- for: Team.ID(),
- userID: UUID()
- )
- }
+ func testGetLegalholdInfo_FailureResponse_InvalidRequest_V0() async throws {
+ try await internalTest_GetLegalholdInfo_Failure(
+ expectedError: TeamsAPIError.invalidRequest,
+ for: .v0,
+ code: .notFound,
+ errorLabel: ""
+ )
}
- func testGetLegalholdStatus_FailureResponse_MemberNotFound_V0() async throws {
- // Given
- let httpClient = try HTTPClientMock(code: .notFound, errorLabel: "no-team-member")
- let sut = TeamsAPIV0(httpClient: httpClient)
-
- // Then
- await XCTAssertThrowsErrorAsync(TeamsAPIError.teamMemberNotFound) {
- // When
- try await sut.getLegalholdStatus(
- for: Team.ID(),
- userID: UUID()
- )
- }
+ func testGetLegalholdInfo_FailureResponse_MemberNotFound_V0() async throws {
+ try await internalTest_GetLegalholdInfo_Failure(
+ expectedError: TeamsAPIError.teamMemberNotFound,
+ for: .v0,
+ code: .notFound,
+ errorLabel: "no-team-member"
+ )
}
// MARK: - V2
@@ -375,19 +369,13 @@ final class TeamsAPITests: XCTestCase {
}
}
- func testGetLegalholdStatus_FailureResponse_InvalidRequest_V4() async throws {
- // Given
- let httpClient = try HTTPClientMock(code: .badRequest, errorLabel: "")
- let sut = TeamsAPIV4(httpClient: httpClient)
-
- // Then
- await XCTAssertThrowsErrorAsync(TeamsAPIError.invalidRequest) {
- // When
- try await sut.getLegalholdStatus(
- for: Team.ID(),
- userID: UUID()
- )
- }
+ func testGetLegalholdInfo_FailureResponse_InvalidRequest_V4() async throws {
+ try await internalTest_GetLegalholdInfo_Failure(
+ expectedError: TeamsAPIError.invalidRequest,
+ for: .v4,
+ code: .badRequest,
+ errorLabel: ""
+ )
}
// MARK: - V5
@@ -404,15 +392,32 @@ final class TeamsAPITests: XCTestCase {
}
}
- func testGetLegalholdStatus_FailureResponse_InvalidRequest_V5() async throws {
+ func testGetLegalholdInfo_FailureResponse_InvalidRequest_V5() async throws {
+ try await internalTest_GetLegalholdInfo_Failure(
+ expectedError: TeamsAPIError.invalidRequest,
+ for: .v5,
+ code: .notFound,
+ errorLabel: ""
+ )
+ }
+
+ private func internalTest_GetLegalholdInfo_Failure(
+ expectedError: any Error & Equatable,
+ for apiVersion: APIVersion,
+ code: HTTPStatusCode,
+ errorLabel: String,
+ file: StaticString = #file,
+ line: UInt = #line
+ ) async throws {
// Given
- let httpClient = try HTTPClientMock(code: .notFound, errorLabel: "")
- let sut = TeamsAPIV5(httpClient: httpClient)
+ let httpClient = try HTTPClientMock(code: code, errorLabel: errorLabel)
+ let sut = apiVersion.buildAPI(client: httpClient)
// Then
- await XCTAssertThrowsErrorAsync(TeamsAPIError.invalidRequest) {
+
+ await XCTAssertThrowsErrorAsync(expectedError) {
// When
- try await sut.getLegalholdStatus(
+ try await sut.getLegalholdInfo(
for: Team.ID(),
userID: UUID()
)
@@ -420,3 +425,10 @@ final class TeamsAPITests: XCTestCase {
}
}
+
+private extension APIVersion {
+ func buildAPI(client: any HTTPClient) -> any TeamsAPI {
+ let builder = TeamsAPIBuilder(httpClient: client)
+ return builder.makeAPI(for: self)
+ }
+}
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v0.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v0.txt
new file mode 100644
index 00000000000..f08e8e90296
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v0.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v1.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v1.txt
new file mode 100644
index 00000000000..c2629df16cf
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v1.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/v1/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v2.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v2.txt
new file mode 100644
index 00000000000..03509c80054
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v2.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/v2/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v3.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v3.txt
new file mode 100644
index 00000000000..f0a8a91c317
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v3.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/v3/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v4.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v4.txt
new file mode 100644
index 00000000000..df69c86a56a
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v4.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/v4/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v5.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v5.txt
new file mode 100644
index 00000000000..bb7d083563a
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v5.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/v5/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v6.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v6.txt
new file mode 100644
index 00000000000..7dca81d74cb
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v6.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/v6/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v0.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v0.txt
new file mode 100644
index 00000000000..f08e8e90296
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v0.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v1.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v1.txt
new file mode 100644
index 00000000000..c2629df16cf
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v1.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/v1/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v2.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v2.txt
new file mode 100644
index 00000000000..03509c80054
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v2.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/v2/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v3.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v3.txt
new file mode 100644
index 00000000000..f0a8a91c317
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v3.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/v3/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v4.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v4.txt
new file mode 100644
index 00000000000..df69c86a56a
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v4.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/v4/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v5.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v5.txt
new file mode 100644
index 00000000000..bb7d083563a
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v5.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/v5/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v6.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v6.txt
new file mode 100644
index 00000000000..7dca81d74cb
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v6.txt
@@ -0,0 +1,4 @@
+- HTTPRequest
+ - path: "/v6/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c"
+ - method: get
+ - body: none
diff --git a/WireAPI/Tests/WireAPITests/APIs/UpdateEventsAPI/UpdateEventsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/UpdateEventsAPI/UpdateEventsAPITests.swift
index 06883db00ce..b083870cc58 100644
--- a/WireAPI/Tests/WireAPITests/APIs/UpdateEventsAPI/UpdateEventsAPITests.swift
+++ b/WireAPI/Tests/WireAPITests/APIs/UpdateEventsAPI/UpdateEventsAPITests.swift
@@ -39,20 +39,20 @@ final class UpdateEventsAPITests: XCTestCase {
func testGetUpdateEvents() async throws {
// Then
- try await createSnapshotter().verifyRequestForAllAPIVersions {
+ try await createSnapshotter().verifyRequestForAllAPIVersions(
// Given
- .withResponses([
+ apiService: .withResponses([
(.ok, "GetUpdateEventsSuccessResponse200_Page1"),
(.ok, "GetUpdateEventsSuccessResponse200_Page2")
- ])
- } when: { sut in
- for try await _ in sut.getUpdateEvents(
- selfClientID: Scaffolding.selfClientID,
- sinceEventID: Scaffolding.lastUpdateEventID
- ) {
- // Nothing to assert here since we're only snapshotting request.
+ ]), when: { sut in
+ for try await _ in sut.getUpdateEvents(
+ selfClientID: Scaffolding.selfClientID,
+ sinceEventID: Scaffolding.lastUpdateEventID
+ ) {
+ // Nothing to assert here since we're only snapshotting request.
+ }
}
- }
+ )
}
// MARK: - Response handling
diff --git a/WireAPI/Tests/WireAPITests/APIs/UserClientsAPI/Resources/GetClientsSuccessResponseV0.json b/WireAPI/Tests/WireAPITests/APIs/UserClientsAPI/Resources/GetOtherUserClientsSuccessResponseV0.json
similarity index 100%
rename from WireAPI/Tests/WireAPITests/APIs/UserClientsAPI/Resources/GetClientsSuccessResponseV0.json
rename to WireAPI/Tests/WireAPITests/APIs/UserClientsAPI/Resources/GetOtherUserClientsSuccessResponseV0.json
diff --git a/WireAPI/Tests/WireAPITests/APIs/UserClientsAPI/Resources/GetSelfClientsSuccessResponseV7.json b/WireAPI/Tests/WireAPITests/APIs/UserClientsAPI/Resources/GetSelfClientsSuccessResponseV7.json
new file mode 100644
index 00000000000..2d111b801a1
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/APIs/UserClientsAPI/Resources/GetSelfClientsSuccessResponseV7.json
@@ -0,0 +1,40 @@
+[
+ {
+ "capabilities": [
+ "legalhold-implicit-consent"
+ ],
+ "class": "phone",
+ "cookie": "string",
+ "id": "string",
+ "label": "string",
+ "location": {
+ "lat": 0,
+ "lon": 0
+ },
+ "mls_public_keys": {
+ "ed25519": "ZXhhbXBsZQo="
+ },
+ "model": "string",
+ "time": "2021-05-12T10:52:02.671Z",
+ "type": "temporary"
+ },
+ {
+ "capabilities": [
+ "legalhold-implicit-consent"
+ ],
+ "class": "phone",
+ "cookie": "string",
+ "id": "string",
+ "label": "string",
+ "location": {
+ "lat": 0,
+ "lon": 0
+ },
+ "mls_public_keys": {
+ "ed25519": "ZXhhbXBsZQo="
+ },
+ "model": "string",
+ "time": "2021-05-12T10:52:02.671Z",
+ "type": "temporary"
+ }
+]
diff --git a/WireAPI/Tests/WireAPITests/APIs/UserClientsAPI/UserClientsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/UserClientsAPI/UserClientsAPITests.swift
index 683d7f06581..a62712a70a4 100644
--- a/WireAPI/Tests/WireAPITests/APIs/UserClientsAPI/UserClientsAPITests.swift
+++ b/WireAPI/Tests/WireAPITests/APIs/UserClientsAPI/UserClientsAPITests.swift
@@ -51,9 +51,9 @@ final class UserClientsAPITests: XCTestCase {
// MARK: - V0
- func testGetSelfUserClients_SuccessResponse_200_V0_And_Next_Versions() async throws {
+ func testGetSelfUserClients_SuccessResponse_200_V0_to_V6() async throws {
try await withThrowingTaskGroup(of: [SelfUserClient].self) { taskGroup in
- let testedVersions = APIVersion.v0.andNextVersions
+ let testedVersions = [APIVersion.v0, .v1, .v2, .v3, .v4, .v5, .v6]
for version in testedVersions {
// Given
@@ -85,7 +85,7 @@ final class UserClientsAPITests: XCTestCase {
for version in testedVersions {
// Given
let apiService = MockAPIServiceProtocol.withResponses([
- (.ok, "GetClientsSuccessResponseV0")
+ (.ok, "GetOtherUserClientsSuccessResponseV0")
])
let sut = version.buildAPI(apiService: apiService)
@@ -106,6 +106,37 @@ final class UserClientsAPITests: XCTestCase {
}
}
+ // MARK: - V7
+
+ func testGetSelfUserClients_SuccessResponse_200_V7_And_Next_Versions() async throws {
+ try await withThrowingTaskGroup(of: [SelfUserClient].self) { taskGroup in
+ let testedVersions = APIVersion.v7.andNextVersions
+
+ for version in testedVersions {
+ // Given
+ let apiService = MockAPIServiceProtocol.withResponses([
+ (.ok, "GetSelfClientsSuccessResponseV7")
+ ])
+
+ let sut = version.buildAPI(apiService: apiService)
+
+ taskGroup.addTask {
+ // When
+ try await sut.getSelfClients()
+ }
+
+ for try await value in taskGroup {
+ for item in value {
+ // Then
+ XCTAssertEqual(item, Scaffolding.userClient)
+ }
+ }
+ }
+ }
+ }
+
+ // MARK: -
+
enum Scaffolding {
static let userClient = SelfUserClient(
id: "string",
diff --git a/WireAPI/Tests/WireAPITests/Authentication/AuthenticationManagerTests.swift b/WireAPI/Tests/WireAPITests/Authentication/AuthenticationManagerTests.swift
index 84b244b7bcf..6149bb800e6 100644
--- a/WireAPI/Tests/WireAPITests/Authentication/AuthenticationManagerTests.swift
+++ b/WireAPI/Tests/WireAPITests/Authentication/AuthenticationManagerTests.swift
@@ -30,7 +30,10 @@ final class AuthenticationManagerTests: XCTestCase {
override func setUpWithError() throws {
cookieStorage = MockCookieStorageProtocol()
backendURL = try XCTUnwrap(URL(string: "https://www.example.com"))
- let networkService = NetworkService(baseURL: backendURL)
+ let networkService = NetworkService(
+ baseURL: backendURL,
+ serverTrustValidator: ServerTrustValidator(pinnedKeys: [])
+ )
networkService.configure(with: .mockURLSession())
sut = AuthenticationManager(
diff --git a/WireAPI/Tests/WireAPITests/Backend/PinnedKeyTests.swift b/WireAPI/Tests/WireAPITests/Backend/PinnedKeyTests.swift
new file mode 100644
index 00000000000..640df70e0d6
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/Backend/PinnedKeyTests.swift
@@ -0,0 +1,56 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import XCTest
+
+@testable import WireAPI
+
+final class PinnedKeyTests: XCTestCase {
+
+ func testInit_withInvalidKeyData() {
+ // GIVEN, WHEN, THEN
+ XCTAssertThrowsError(
+ try PinnedKey(
+ key: Data(),
+ hosts: [.equals("foo.example.com")]
+ )
+ ) { error in
+ XCTAssertEqual(error as? PinnedKey.Failure, .invalidKeyData)
+ }
+ }
+
+ func testMatchesHost() throws {
+ // GIVEN
+ let sut = try PinnedKey(
+ key: try PublicKeys.wire,
+ hosts: [
+ .equals("foo.example.com"),
+ .equals("bar.example.com"),
+ .endsWith("example.net")
+ ]
+ )
+
+ // WHEN, THEN
+ XCTAssertTrue(sut.matches(host: "bar.example.com"))
+ XCTAssertFalse(sut.matches(host: "something.bar.example.com"))
+ XCTAssertTrue(sut.matches(host: "example.net"))
+ XCTAssertTrue(sut.matches(host: "something.example.net"))
+ XCTAssertFalse(sut.matches(host: "example.net.something"))
+ }
+
+}
diff --git a/WireAPI/Tests/WireAPITests/Backend/ProxySettingsTests.swift b/WireAPI/Tests/WireAPITests/Backend/ProxySettingsTests.swift
new file mode 100644
index 00000000000..d7646960e1d
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/Backend/ProxySettingsTests.swift
@@ -0,0 +1,59 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import XCTest
+
+@testable import WireAPI
+
+final class ProxySettingsTests: XCTestCase {
+
+ func testProxyDictionary_whenUnauthenticated() throws {
+ // GIVEN
+ let sut = ProxySettings.unauthenticated(host: "Host", port: 10)
+
+ // WHEN
+ let result = sut.proxyDictionary()
+
+ // THEN
+ XCTAssertEqual(result.count, 5)
+ XCTAssertEqual(result["SOCKSEnable"] as? Int, 1)
+ XCTAssertEqual(result["SOCKSProxy"] as? String, "Host")
+ XCTAssertEqual(result["SOCKSPort"] as? Int, 10)
+ XCTAssertEqual(result[kCFProxyTypeKey] as? String, kCFProxyTypeSOCKS as String)
+ XCTAssertEqual(result[kCFStreamPropertySOCKSVersion] as? String, kCFStreamSocketSOCKSVersion5 as String)
+ }
+
+ func testProxyDictionary_whenAuthenticated() throws {
+ // GIVEN
+ let sut = ProxySettings.authenticated(host: "Host", port: 10, username: "User", password: "Password")
+
+ // WHEN
+ let result = sut.proxyDictionary()
+
+ // THEN
+ XCTAssertEqual(result.count, 7)
+ XCTAssertEqual(result["SOCKSEnable"] as? Int, 1)
+ XCTAssertEqual(result["SOCKSProxy"] as? String, "Host")
+ XCTAssertEqual(result["SOCKSPort"] as? Int, 10)
+ XCTAssertEqual(result[kCFProxyTypeKey] as? String, kCFProxyTypeSOCKS as String)
+ XCTAssertEqual(result[kCFStreamPropertySOCKSVersion] as? String, kCFStreamSocketSOCKSVersion5 as String)
+ XCTAssertEqual(result[kCFStreamPropertySOCKSUser] as? String, "User")
+ XCTAssertEqual(result[kCFStreamPropertySOCKSPassword] as? String, "Password")
+ }
+
+}
diff --git a/WireAPI/Tests/WireAPITests/Backend/Resources/certificates.json b/WireAPI/Tests/WireAPITests/Backend/Resources/certificates.json
new file mode 100644
index 00000000000..0e8e10ff802
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/Backend/Resources/certificates.json
@@ -0,0 +1,15 @@
+
+{
+ "wire" : [
+ "MIIGwjCCBaqgAwIBAgIQBWiPdednYef/Bnn3xT49PTANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTMwMQYDVQQDEypEaWdpQ2VydCBHbG9iYWwgRzIgVExTIFJTQSBTSEEyNTYgMjAyMCBDQTEwHhcNMjQwMzE4MDAwMDAwWhcNMjUwNDA5MjM1OTU5WjBKMQswCQYDVQQGEwJDSDEMMAoGA1UEBxMDWnVnMRgwFgYDVQQKEw9XaXJlIFN3aXNzIEdtYkgxEzARBgNVBAMMCioud2lyZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt5jMFa6+dUph+A01fd1WNSeohW2XhepCcJxjqb+xYzXlNMrRuj0UqczE0A+0PMHpWJG+lmwoR59fymLXklyzi5mK5nzUhJXVurG2myMnnpiN6Z730NxrlyTfmlOFi4rqNny8bqkmJj2ZFj2cZp2J3ipYvu7AB6gifHaY4zsd6kIKHY05d34SNDiwGx+Bv6RatxVCYHO8sc9QOjKSb+b4G8vZ4nWeM82Iz8ah5duYhbVYzeJ+5xgmgP2D5Xk18d8A2tW7bDhhwsNp3QLzk1vxTWyAU2SuA6rOF3/XEeiTW47KOh4tMgcdiSvK9sESZ2Xq/5/YnUQzT4WP2+x4jZNitAgMBAAGjggOTMIIDjzAfBgNVHSMEGDAWgBR0hYDAZsffN97PvSk3qgMdvu3NFzAdBgNVHQ4EFgQU/0iA8JzB4tDtwNB/3NyYfCtfTZwwHwYDVR0RBBgwFoIKKi53aXJlLmNvbYIId2lyZS5jb20wPgYDVR0gBDcwNTAzBgZngQwBAgIwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwgZ8GA1UdHwSBlzCBlDBIoEagRIZCaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsRzJUTFNSU0FTSEEyNTYyMDIwQ0ExLTEuY3JsMEigRqBEhkJodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxHMlRMU1JTQVNIQTI1NjIwMjBDQTEtMS5jcmwwgYcGCCsGAQUFBwEBBHsweTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFEGCCsGAQUFBzAChkVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxHMlRMU1JTQVNIQTI1NjIwMjBDQTEtMS5jcnQwDAYDVR0TAQH/BAIwADCCAYEGCisGAQQB1nkCBAIEggFxBIIBbQFrAHcAzxFW7tUufK/zh1vZaS6b6RpxZ0qwF+ysAdJbd87MOwgAAAGOUfIjXAAABAMASDBGAiEA7VZPfsWpAqcB5BQC35MaVEwImPGzfCtPRVev3rIvrr4CIQCf37utuWmC0LTwDDMwBBW0S2eOipS9GwN19TmLkMJmQAB3AH1ZHhLheCp7HGFnfF79+NCHXBSgTpWeuQMv2Q6MLnm4AAABjlHyI0sAAAQDAEgwRgIhANT+RYxLTrc2ikPY1eArpA1A1Qecez17aBhVpG5QH6a4AiEA1Kvl1cYjSNG6Pyx46VAhaa/i8Vla2iqNDdFTrxc0H4oAdwDm0jFjQHeMwRBBBtdxuc7B0kD2loSG+7qHMh39HjeOUAAAAY5R8iNsAAAEAwBIMEYCIQD3ZzpWw6NPfncsNvh9xEr9pKGfHexeqOAqAzWtVUAViAIhAOF3ncMWaqjL5r1sy/wmWWmCez8HdUzmvO3UB5naH7a8MA0GCSqGSIb3DQEBCwUAA4IBAQAZ1G6F6nXrwhNohiKJIuS5uD5eOXXCkcKvq3HP5ydRIudX6CNMa88nMd3STJcWuJZz557Aioec/ETyCarXKPyLvRrJLnhnCO4KCJfVko6zkki0VHC7el5PfqN+7rRGavteQz3IP2nmbZciW2OoaF2fp4l9Q6IDixl5PUHVPs0nU2gCYdAgoFGGXZk9La5NiHThO92B/KHCvtuXZixOQ+sgFW1g0guV81T45qcP0wAxDDZx1twhM/EBqW6eadcmLq1AWQ8oebFcDysZRHKJ4xoEyR/EU9wrv22WonS7GfylR9ZrwxUx8d2fLaImbIHDTo5oxqPcp/WgpVR8VJrprEmX",
+ "MIIEyDCCA7CgAwIBAgIQDPW9BitWAvR6uFAsI8zwZjANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0yMTAzMzAwMDAwMDBaFw0zMTAzMjkyMzU5NTlaMFkxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMzAxBgNVBAMTKkRpZ2lDZXJ0IEdsb2JhbCBHMiBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMz3EGJPprtjb+2QUlbFbSd7ehJWivH0+dbn4Y+9lavyYEEVcNsSAPonCrVXOFt9slGTcZUOakGUWzUb+nv6u8W+JDD+Vu/E832X4xT1FE3LpxDyFuqrIvAxIhFhaZAmunjZlx/jfWardUSVc8is/+9dCopZQ+GssjoP80j812s3wWPc3kbW20X+fSP9kOhRBx5Ro1/tSUZUfyyIxfQTnJcVPAPooTncaQwywa8WV0yUR0J8osicfebUTVSvQpmowQTCd5zWSOTOEeAqgJnwQ3DPP3Zr0UxJqyRewg2C/Uaoq2yTzGJSQnWS+Jr6Xl6ysGHlHx+5fwmY6D36g39HaaECAwEAAaOCAYIwggF+MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFHSFgMBmx9833s+9KTeqAx2+7c0XMB8GA1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdgYIKwYBBQUHAQEEajBoMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDA9BgNVHSAENjA0MAsGCWCGSAGG/WwCATAHBgVngQwBATAIBgZngQwBAgEwCAYGZ4EMAQICMAgGBmeBDAECAzANBgkqhkiG9w0BAQsFAAOCAQEAkPFwyyiXaZd8dP3A+iZ7U6utzWX9upwGnIrXWkOH7U1MVl+twcW1BSAuWdH/SvWgKtiwla3JLko716f2b4gp/DA/JIS7w7d7kwcsr4drdjPtAFVSslme5LnQ89/nD/7d+MS5EHKBCQRfz5eeLjJ1js+aWNJXMX43AYGyZm0pGrFmCW3RbpD0ufovARTFXFZkAdl9h6g4U5+LXUZtXMYnhIHUfoyMo5tS58aI7Dd8KvvwVVo4chDYABPPTHPbqjc1qCmBaZx2vN4Ye5DUys/vZwP9BFohFrH/6j/f3IL16/RZkiMNJCqVJUzKoZHm1Lesh3Sz8W2jmdv51b2EQJ8HmA==",
+ ],
+ "other" : [
+ "MIIOCzCCDPOgAwIBAgIRAMg6iqXyEfImEMVm/IELGKQwDQYJKoZIhvcNAQELBQAwOzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFUdvb2dsZSBUcnVzdCBTZXJ2aWNlczEMMAoGA1UEAxMDV1IyMB4XDTI0MTEwNDA4Mzc0N1oXDTI1MDEyNzA4Mzc0NlowFzEVMBMGA1UEAwwMKi5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETrFzvW664cnLp0nsu73DWXOluuWCJdoFXaGl3/kltTQ+DuITdA/VC3vXEIe8QNY7MxnOeK2w3dOdfW4QpQqJJqOCC/cwggvzMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRFC6MFCp3z9dQlFrzA3HibW/pgxDAfBgNVHSMEGDAWgBTeGx7teRXUPjckwyG77DQ5bUKyMDBYBggrBgEFBQcBAQRMMEowIQYIKwYBBQUHMAGGFWh0dHA6Ly9vLnBraS5nb29nL3dyMjAlBggrBgEFBQcwAoYZaHR0cDovL2kucGtpLmdvb2cvd3IyLmNydDCCCc0GA1UdEQSCCcQwggnAggwqLmdvb2dsZS5jb22CFiouYXBwZW5naW5lLmdvb2dsZS5jb22CCSouYmRuLmRldoIVKi5vcmlnaW4tdGVzdC5iZG4uZGV2ghIqLmNsb3VkLmdvb2dsZS5jb22CGCouY3Jvd2Rzb3VyY2UuZ29vZ2xlLmNvbYIYKi5kYXRhY29tcHV0ZS5nb29nbGUuY29tggsqLmdvb2dsZS5jYYILKi5nb29nbGUuY2yCDiouZ29vZ2xlLmNvLmlugg4qLmdvb2dsZS5jby5qcIIOKi5nb29nbGUuY28udWuCDyouZ29vZ2xlLmNvbS5hcoIPKi5nb29nbGUuY29tLmF1gg8qLmdvb2dsZS5jb20uYnKCDyouZ29vZ2xlLmNvbS5jb4IPKi5nb29nbGUuY29tLm14gg8qLmdvb2dsZS5jb20udHKCDyouZ29vZ2xlLmNvbS52boILKi5nb29nbGUuZGWCCyouZ29vZ2xlLmVzggsqLmdvb2dsZS5mcoILKi5nb29nbGUuaHWCCyouZ29vZ2xlLml0ggsqLmdvb2dsZS5ubIILKi5nb29nbGUucGyCCyouZ29vZ2xlLnB0gg8qLmdvb2dsZWFwaXMuY26CESouZ29vZ2xldmlkZW8uY29tggwqLmdzdGF0aWMuY26CECouZ3N0YXRpYy1jbi5jb22CD2dvb2dsZWNuYXBwcy5jboIRKi5nb29nbGVjbmFwcHMuY26CEWdvb2dsZWFwcHMtY24uY29tghMqLmdvb2dsZWFwcHMtY24uY29tggxna2VjbmFwcHMuY26CDiouZ2tlY25hcHBzLmNughJnb29nbGVkb3dubG9hZHMuY26CFCouZ29vZ2xlZG93bmxvYWRzLmNughByZWNhcHRjaGEubmV0LmNughIqLnJlY2FwdGNoYS5uZXQuY26CEHJlY2FwdGNoYS1jbi5uZXSCEioucmVjYXB0Y2hhLWNuLm5ldIILd2lkZXZpbmUuY26CDSoud2lkZXZpbmUuY26CEWFtcHByb2plY3Qub3JnLmNughMqLmFtcHByb2plY3Qub3JnLmNughFhbXBwcm9qZWN0Lm5ldC5jboITKi5hbXBwcm9qZWN0Lm5ldC5jboIXZ29vZ2xlLWFuYWx5dGljcy1jbi5jb22CGSouZ29vZ2xlLWFuYWx5dGljcy1jbi5jb22CF2dvb2dsZWFkc2VydmljZXMtY24uY29tghkqLmdvb2dsZWFkc2VydmljZXMtY24uY29tghFnb29nbGV2YWRzLWNuLmNvbYITKi5nb29nbGV2YWRzLWNuLmNvbYIRZ29vZ2xlYXBpcy1jbi5jb22CEyouZ29vZ2xlYXBpcy1jbi5jb22CFWdvb2dsZW9wdGltaXplLWNuLmNvbYIXKi5nb29nbGVvcHRpbWl6ZS1jbi5jb22CEmRvdWJsZWNsaWNrLWNuLm5ldIIUKi5kb3VibGVjbGljay1jbi5uZXSCGCouZmxzLmRvdWJsZWNsaWNrLWNuLm5ldIIWKi5nLmRvdWJsZWNsaWNrLWNuLm5ldIIOZG91YmxlY2xpY2suY26CECouZG91YmxlY2xpY2suY26CFCouZmxzLmRvdWJsZWNsaWNrLmNughIqLmcuZG91YmxlY2xpY2suY26CEWRhcnRzZWFyY2gtY24ubmV0ghMqLmRhcnRzZWFyY2gtY24ubmV0gh1nb29nbGV0cmF2ZWxhZHNlcnZpY2VzLWNuLmNvbYIfKi5nb29nbGV0cmF2ZWxhZHNlcnZpY2VzLWNuLmNvbYIYZ29vZ2xldGFnc2VydmljZXMtY24uY29tghoqLmdvb2dsZXRhZ3NlcnZpY2VzLWNuLmNvbYIXZ29vZ2xldGFnbWFuYWdlci1jbi5jb22CGSouZ29vZ2xldGFnbWFuYWdlci1jbi5jb22CGGdvb2dsZXN5bmRpY2F0aW9uLWNuLmNvbYIaKi5nb29nbGVzeW5kaWNhdGlvbi1jbi5jb22CJCouc2FmZWZyYW1lLmdvb2dsZXN5bmRpY2F0aW9uLWNuLmNvbYIWYXBwLW1lYXN1cmVtZW50LWNuLmNvbYIYKi5hcHAtbWVhc3VyZW1lbnQtY24uY29tggtndnQxLWNuLmNvbYINKi5ndnQxLWNuLmNvbYILZ3Z0Mi1jbi5jb22CDSouZ3Z0Mi1jbi5jb22CCzJtZG4tY24ubmV0gg0qLjJtZG4tY24ubmV0ghRnb29nbGVmbGlnaHRzLWNuLm5ldIIWKi5nb29nbGVmbGlnaHRzLWNuLm5ldIIMYWRtb2ItY24uY29tgg4qLmFkbW9iLWNuLmNvbYIUZ29vZ2xlc2FuZGJveC1jbi5jb22CFiouZ29vZ2xlc2FuZGJveC1jbi5jb22CHiouc2FmZW51cC5nb29nbGVzYW5kYm94LWNuLmNvbYINKi5nc3RhdGljLmNvbYIUKi5tZXRyaWMuZ3N0YXRpYy5jb22CCiouZ3Z0MS5jb22CESouZ2NwY2RuLmd2dDEuY29tggoqLmd2dDIuY29tgg4qLmdjcC5ndnQyLmNvbYIQKi51cmwuZ29vZ2xlLmNvbYIWKi55b3V0dWJlLW5vY29va2llLmNvbYILKi55dGltZy5jb22CC2FuZHJvaWQuY29tgg0qLmFuZHJvaWQuY29tghMqLmZsYXNoLmFuZHJvaWQuY29tggRnLmNuggYqLmcuY26CBGcuY2+CBiouZy5jb4IGZ29vLmdsggp3d3cuZ29vLmdsghRnb29nbGUtYW5hbHl0aWNzLmNvbYIWKi5nb29nbGUtYW5hbHl0aWNzLmNvbYIKZ29vZ2xlLmNvbYISZ29vZ2xlY29tbWVyY2UuY29tghQqLmdvb2dsZWNvbW1lcmNlLmNvbYIIZ2dwaHQuY26CCiouZ2dwaHQuY26CCnVyY2hpbi5jb22CDCoudXJjaGluLmNvbYIIeW91dHUuYmWCC3lvdXR1YmUuY29tgg0qLnlvdXR1YmUuY29tghFtdXNpYy55b3V0dWJlLmNvbYITKi5tdXNpYy55b3V0dWJlLmNvbYIUeW91dHViZWVkdWNhdGlvbi5jb22CFioueW91dHViZWVkdWNhdGlvbi5jb22CD3lvdXR1YmVraWRzLmNvbYIRKi55b3V0dWJla2lkcy5jb22CBXl0LmJlggcqLnl0LmJlghphbmRyb2lkLmNsaWVudHMuZ29vZ2xlLmNvbYITKi5hbmRyb2lkLmdvb2dsZS5jboISKi5jaHJvbWUuZ29vZ2xlLmNughYqLmRldmVsb3BlcnMuZ29vZ2xlLmNuMBMGA1UdIAQMMAowCAYGZ4EMAQIBMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jLnBraS5nb29nL3dyMi85VVZiTjB3NUU2WS5jcmwwggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdgDPEVbu1S58r/OHW9lpLpvpGnFnSrAX7KwB0lt3zsw7CAAAAZL2ibUlAAAEAwBHMEUCIQClduzISm51SeFjpmbBW2U2zwk7fTiaWsvddRbHLZH06gIgT3wbf1ZoTtBVj/Jl3IdjFEl2bcEL4w5kmVPG/akmNeEAdgDm0jFjQHeMwRBBBtdxuc7B0kD2loSG+7qHMh39HjeOUAAAAZL2ibULAAAEAwBHMEUCIQC40RSwIuAXPdf9rShZXGrA6elTY/2cmNmznJ5N1ZWq8QIgG4cgjpSbYYIzZQ3oggZxnyHc8hv55AeQv6F2xpZVt2UwDQYJKoZIhvcNAQELBQADggEBABflcY9wxTtmQuEouJqRhZ1D9Ds2Pl2cB3i0aA/Vbrcc/rcgW+RPUmN5d8Kh4WdeqCIMYpNNambI4n9XPnATN78CE/Q568b5msgTZrU5Fo8OzuDUdi9cjo3BH7S6un6o8odYXvUzdHuHNDUvQb8jhdbNOYbXW6fWfWBrlq1qwploqWeJghFvLOJU9+GQlgHv9+mO91tz/5DQfYgsgg7z9mQ+3KHHkgUrN5lW0y9huzGx96/exehf89FmpwPlRn82OWvX3qWhvq58acyqjGFpsGzGhAtWqBq8Ms184g6ndrBJVTsFye7ov5a3RHuhN/wbLhPJf1CzHRBhuGH2//Hko5g=",
+ "MIIFCzCCAvOgAwIBAgIQf/AFoHxM3tEArZ1mpRB7mDANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIwMTQwMDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMQwwCgYDVQQDEwNXUjIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCp/5x/RR5wqFOfytnlDd5GV1d9vI+aWqxG8YSau5HbyfsvAfuSCQAWXqAc+MGr+XgvSszYhaLYWTwO0xj7sfUkDSbutltkdnwUxy96zqhMt/TZCPzfhyM1IKjiaeKMTj+xWfpgoh6zySBTGYLKNlNtYE3pAJH8do1cCA8Kwtzxc2vFE24KT3rC8gIcLrRjg9ox9i11MLL7q8Ju26nADrn5Z9TDJVd06wW06Y613ijNzHoU5HEDy01hLmFXxRmpC5iEGuh5KdmyjS//V2pm4M6rlagplmNwEmceOuHbsCFx13ye/aoXbv4r+zgXFNFmp6+atXDMyGOBOozAKql2N87jAgMBAAGjgf4wgfswDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBTeGx7teRXUPjckwyG77DQ5bUKyMDAfBgNVHSMEGDAWgBTkrysmcRorSCeFL1JmLO/wiRNxPjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAKGGGh0dHA6Ly9pLnBraS5nb29nL3IxLmNydDArBgNVHR8EJDAiMCCgHqAchhpodHRwOi8vYy5wa2kuZ29vZy9yL3IxLmNybDATBgNVHSAEDDAKMAgGBmeBDAECATANBgkqhkiG9w0BAQsFAAOCAgEARXWL5R87RBOWGqtY8TXJbz3S0DNKhjO6V1FP7sQ02hYSTL8Tnw3UVOlIecAwPJQl8hr0ujKUtjNyC4XuCRElNJThb0Lbgpt7fyqaqf9/qdLeSiDLs/sDA7j4BwXaWZIvGEaYzq9yviQmsR4ATb0IrZNBRAq7x9UBhb+TV+PfdBJTDhEl05vc3ssnbrPCuTNiOcLgNeFbpwkuGcuRKnZc8d/KI4RApW//mkHgte8y0YWuryUJ8GLFbsLIbjL9uNrizkqRSvOFVU6xddZIMy9vhNkSXJ/UcZhjJY1pXAprffJBvei7j+Qi151lRehMCofa6WBmiA4fx+FOVsV2/7R6V2nyAiIJJkEd2nSi5SnzxJrlXdaqev3htytmOPvoKWa676ATL/hzfvDaQBEcXd2Ppvy+275W+DKcH0FBbX62xevGiza3F4ydzxl6NJ8hk8R+dDXSqv1MbRT1ybB5W0k8878XSOjvmiYTDIfyc9acxVJrY/cykHipa+te1pOhv7wYPYtZ9orGBV5SGOJm4NrB3K1aJar0RfzxC3ikr7Dyc6QwqDTBU39CluVIQeuQRgwG3MuSxl7zRERDRilGoKb8uY45JzmxWuKxrfwT/478JuHU/oTxUFqOl2stKnn7QGTq8z29W+GgBLCXSBxC9epaHM0myFH/FJlniXJfHeytWt0=",
+ "MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBXMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UECxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYxOTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwSiV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351kKSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZDrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zkj5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esWCruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35EiEua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbapsZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAfBgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUHMAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6AloCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAyMAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIFAwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy+qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvid0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8="
+ ],
+ "invalid" : [
+ "MIIHXDCCBkSgAwIBAgIIU0E9kXjSjCcwDQYJKoZIhvcNAQELBQAwSTELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRlcm5ldCBBdXRob3JpdHkgRzIwHhcNMTcwNDI3MDgzMDAwWhcNMTcwNzIwMDgzMDAwWjBmMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEVMBMGA1UEAwwMKi5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvU0whUatntNonctaUdJSys2T0fw+Idy/GS0xd0dWIviAdKTHrFuz3+9MTD8ZRTU1bsWl3BnS7ZMeEoOexFZeb6OCBPQwggTwMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjALBgNVHQ8EBAMCB4AwggOzBgNVHREEggOqMIIDpoIMKi5nb29nbGUuY29tgg0qLmFuZHJvaWQuY29tghYqLmFwcGVuZ2luZS5nb29nbGUuY29tghIqLmNsb3VkLmdvb2dsZS5jb22CDiouZ2NwLmd2dDIuY29tghYqLmdvb2dsZS1hbmFseXRpY3MuY29tggsqLmdvb2dsZS5jYYILKi5nb29nbGUuY2yCDiouZ29vZ2xlLmNvLmlugg4qLmdvb2dsZS5jby5qcIIOKi5nb29nbGUuY28udWuCDyouZ29vZ2xlLmNvbS5hcoIPKi5nb29nbGUuY29tLmF1gg8qLmdvb2dsZS5jb20uYnKCDyouZ29vZ2xlLmNvbS5jb4IPKi5nb29nbGUuY29tLm14gg8qLmdvb2dsZS5jb20udHKCDyouZ29vZ2xlLmNvbS52boILKi5nb29nbGUuZGWCCyouZ29vZ2xlLmVzggsqLmdvb2dsZS5mcoILKi5nb29nbGUuaHWCCyouZ29vZ2xlLml0ggsqLmdvb2dsZS5ubIILKi5nb29nbGUucGyCCyouZ29vZ2xlLnB0ghIqLmdvb2dsZWFkYXBpcy5jb22CDyouZ29vZ2xlYXBpcy5jboIUKi5nb29nbGVjb21tZXJjZS5jb22CESouZ29vZ2xldmlkZW8uY29tggwqLmdzdGF0aWMuY26CDSouZ3N0YXRpYy5jb22CCiouZ3Z0MS5jb22CCiouZ3Z0Mi5jb22CFCoubWV0cmljLmdzdGF0aWMuY29tggwqLnVyY2hpbi5jb22CECoudXJsLmdvb2dsZS5jb22CFioueW91dHViZS1ub2Nvb2tpZS5jb22CDSoueW91dHViZS5jb22CFioueW91dHViZWVkdWNhdGlvbi5jb22CCyoueXRpbWcuY29tghphbmRyb2lkLmNsaWVudHMuZ29vZ2xlLmNvbYILYW5kcm9pZC5jb22CG2RldmVsb3Blci5hbmRyb2lkLmdvb2dsZS5jboIcZGV2ZWxvcGVycy5hbmRyb2lkLmdvb2dsZS5jboIEZy5jb4IGZ29vLmdsghRnb29nbGUtYW5hbHl0aWNzLmNvbYIKZ29vZ2xlLmNvbYISZ29vZ2xlY29tbWVyY2UuY29tghhzb3VyY2UuYW5kcm9pZC5nb29nbGUuY26CCnVyY2hpbi5jb22CCnd3dy5nb28uZ2yCCHlvdXR1LmJlggt5b3V0dWJlLmNvbYIUeW91dHViZWVkdWNhdGlvbi5jb20waAYIKwYBBQUHAQEEXDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0GA1UdDgQWBBT9WhKAC80EX/JNWSgDcCNXGUD4NzAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMCEGA1UdIAQaMBgwDAYKKwYBBAHWeQIFATAIBgZngQwBAgIwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL3BraS5nb29nbGUuY29tL0dJQUcyLmNybDANBgkqhkiG9w0BAQsFAAOCAQEAIcipac4Cm3i9wDM6dVE6d1So6HBDSrJsjO+MOvF2BVeXSz7kZE/9qNAnb/gPiNDwak11ebI6WPWzovsKR8rFRpCFosWAOW+0owY/mDDhhc+cjpObpOqWX8LMJK4LUQhrf7WE2OboHZgBLpiiuWnliPWFP4Zc8tkHGkCE+H67cjjA8EbVjFoTFyGb3E9d+fy4vjwte/a2zOgtb9a798PXPFh4DPEANr24yIo4Og/TPu/cGOCOn8dmfgnH1Dyje8Pr79IIGKw/d9jpAQhlFZYKSyTzLfHzv/7dhQuIUkAy3t0wuDC3m+TMILHRi1YPvnbpW5J8hNoySW6YP8QIQhOrPg=="
+ ]
+}
diff --git a/WireAPI/Tests/WireAPITests/Backend/Resources/public-keys.json b/WireAPI/Tests/WireAPITests/Backend/Resources/public-keys.json
new file mode 100644
index 00000000000..bc3edd08600
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/Backend/Resources/public-keys.json
@@ -0,0 +1,4 @@
+{
+ "wire" : "MIIE/jCCA+agAwIBAgIQBmeNK7xaqmvwoGsKbGiEuzANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5EaWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTcxMjEyMDAwMDAwWhcNMTkwMjAxMTIwMDAwWjBKMQswCQYDVQQGEwJDSDEMMAoGA1UEBxMDWnVnMRgwFgYDVQQKEw9XaXJlIFN3aXNzIEdtYkgxEzARBgNVBAMMCioud2lyZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt5jMFa6+dUph+A01fd1WNSeohW2XhepCcJxjqb+xYzXlNMrRuj0UqczE0A+0PMHpWJG+lmwoR59fymLXklyzi5mK5nzUhJXVurG2myMnnpiN6Z730NxrlyTfmlOFi4rqNny8bqkmJj2ZFj2cZp2J3ipYvu7AB6gifHaY4zsd6kIKHY05d34SNDiwGx+Bv6RatxVCYHO8sc9QOjKSb+b4G8vZ4nWeM82Iz8ah5duYhbVYzeJ+5xgmgP2D5Xk18d8A2tW7bDhhwsNp3QLzk1vxTWyAU2SuA6rOF3/XEeiTW47KOh4tMgcdiSvK9sESZ2Xq/5/YnUQzT4WP2+x4jZNitAgMBAAGjggHbMIIB1zAfBgNVHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQU/0iA8JzB4tDtwNB/3NyYfCtfTZwwHwYDVR0RBBgwFoIKKi53aXJlLmNvbYIId2lyZS5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBrBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1zaGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYBBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAc6v6cf/EQmmeGU2nC87F6QgEIAIL3svgabImao3f01QFVxC0XX2Cf9+wofijspqq5Uj80nb04o5HNnZWX1agJmqp8jTYH2hw4+uiwFCld0QEptHMrCwEAyyouf0/cl2dfRv2V8m29W6Qb4+7pc1rEbFLl3fywmjgzpGkr1+cKE7pwkpgKqhulKkE4CDXant0Slj7cvDisSPy/kInJ5uHI29Z/SBCpACyHah6lkdIQyTo4uem1XH6i5UP9sTvCAZl0acHcPsvcJ50LeJvJC7sPNXr60xZYLIK5LIVrSSRhxtOB1WPMbzIQc5bF2LcSjXJNvXA5+RCO79om91mlheqPQ==",
+ "some" : "MIIE/jCCA+agAwIBAgIQBmeNK7xaqmvwoGsKbGiEuzANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5EaWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTcxMjEyMDAwMDAwWhcNMTkwMjAxMTIwMDAwWjBKMQswCQYDVQQGEwJDSDEMMAoGA1UEBxMDWnVnMRgwFgYDVQQKEw9XaXJlIFN3aXNzIEdtYkgxEzARBgNVBAMMCioud2lyZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt5jMFa6+dUph+A01fd1WNSeohW2XhepCcJxjqb+xYzXlNMrRuj0UqczE0A+0PMHpWJG+lmwoR59fymLXklyzi5mK5nzUhJXVurG2myMnnpiN6Z730NxrlyTfmlOFi4rqNny8bqkmJj2ZFj2cZp2J3ipYvu7AB6gifHaY4zsd6kIKHY05d34SNDiwGx+Bv6RatxVCYHO8sc9QOjKSb+b4G8vZ4nWeM82Iz8ah5duYhbVYzeJ+5xgmgP2D5Xk18d8A2tW7bDhhwsNp3QLzk1vxTWyAU2SuA6rOF3/XEeiTW47KOh4tMgcdiSvK9sESZ2Xq/5/YnUQzT4WP2+x4jZNitAgMBAAGjggHbMIIB1zAfBgNVHSMEGDAWgBQPgGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQU/0iA8JzB4tDtwNB/3NyYfCtfTZwwHwYDVR0RBBgwFoIKKi53aXJlLmNvbYIId2lyZS5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBrBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1zaGEyLWc2LmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBAgIwfAYIKwYBBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAc6v6cf/EQmmeGU2nC87F6QgEIAIL3svgabImao3f01QFVxC0XX2Cf9+wofijspqq5Uj80nb04o5HNnZWX1agJmqp8jTYH2hw4+uiwFCld0QEptHMrCwEAyyouf0/cl2dfRv2V8m29W6Qb4+7pc1rEbFLl3fywmjgzpGkr1+cKE7pwkpgKqhulKkE4CDXant0Slj7cvDisSPy/kInJ5uHI29Z/SBCpACyHah6lkdIQyTo4uem1XH6i5UP9sTvCAZl0acHcPsvcJ50LeJvJC7sPNXr60xZYLIK5LIVrSSRhxtOB1WPMbzIQc5bF2LcSjXJNvXA5+RCO79om91mlheqPQ=="
+}
diff --git a/WireAPI/Tests/WireAPITests/Backend/ServerTrustValidatorTests.swift b/WireAPI/Tests/WireAPITests/Backend/ServerTrustValidatorTests.swift
new file mode 100644
index 00000000000..1606d1e9370
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/Backend/ServerTrustValidatorTests.swift
@@ -0,0 +1,66 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireTestingPackage
+import XCTest
+
+@testable import WireAPI
+
+final class ServerTrustValidatorTests: XCTestCase {
+
+ func testValidate_whenNoMatchingHosts() async throws {
+ let sut = ServerTrustValidator(
+ pinnedKeys: [
+ try PinnedKey(key: PublicKeys.wire, hosts: [.equals("prod-nginz-https.wire.com")])
+ ]
+ )
+
+ try await sut.validate(trust: .other, host: "example.com")
+ }
+
+ func testValidate_whenInvalidServerTrust() async throws {
+ // GIVEN
+ let sut = ServerTrustValidator(
+ pinnedKeys: [
+ try PinnedKey(key: PublicKeys.wire, hosts: [.equals("prod-nginz-https.wire.com")])
+ ]
+ )
+
+ // WHEN, THEN
+ await XCTAssertThrowsErrorAsync(
+ ServerTrustValidator.Failure.evaluatingServerTrustFailed,
+ when: { try await sut.validate(trust: .invalid, host: "prod-nginz-https.wire.com") }
+ )
+ }
+
+ func testValidate_whenNoMatchingPublicKey() async throws {
+ // GIVEN
+ let sut = ServerTrustValidator(
+ pinnedKeys: [
+ try PinnedKey(key: PublicKeys.wire, hosts: [.equals("example.com")])
+ ]
+ )
+
+ // WHEN, THEN
+ await XCTAssertThrowsErrorAsync(
+ ServerTrustValidator.Failure.noMatchingPublicKey,
+ when: { try await sut.validate(trust: .other, host: "example.com") }
+ )
+ }
+
+}
diff --git a/WireAPI/Tests/WireAPITests/Helpers/APIServiceSnapshotHelper.swift b/WireAPI/Tests/WireAPITests/Helpers/APIServiceSnapshotHelper.swift
index 6a1805f75b6..80a633431c4 100644
--- a/WireAPI/Tests/WireAPITests/Helpers/APIServiceSnapshotHelper.swift
+++ b/WireAPI/Tests/WireAPITests/Helpers/APIServiceSnapshotHelper.swift
@@ -51,7 +51,7 @@ struct APIServiceSnapshotHelper {
/// - line: The line invoking the test.
func verifyRequestForAllAPIVersions(
- apiService: (() throws -> MockAPIServiceProtocol)? = nil,
+ apiService: @autoclosure () throws -> MockAPIServiceProtocol = .withResponses([]),
when block: (API) async throws -> Void,
file: StaticString = #filePath,
function: String = #function,
@@ -59,7 +59,7 @@ struct APIServiceSnapshotHelper {
) async throws {
try await verifyRequest(
for: APIVersion.allCases,
- apiService: apiService,
+ apiService: apiService(),
when: block,
file: file,
function: function,
@@ -83,7 +83,7 @@ struct APIServiceSnapshotHelper {
func verifyRequest(
for apiVersions: any Sequence,
- apiService: (() throws -> MockAPIServiceProtocol)? = nil,
+ apiService: @autoclosure () throws -> MockAPIServiceProtocol = .withResponses([]),
when block: (API) async throws -> Void,
file: StaticString = #filePath,
function: String = #function,
@@ -92,7 +92,7 @@ struct APIServiceSnapshotHelper {
for apiVersion in apiVersions {
try await verifyRequest(
apiVersion: apiVersion,
- apiService: apiService?() ?? .withResponses([]),
+ apiService: apiService(),
when: block,
file: file,
function: function,
diff --git a/WireAPI/Tests/WireAPITests/Helpers/HTTPRequestSnapshotHelper.swift b/WireAPI/Tests/WireAPITests/Helpers/HTTPRequestSnapshotHelper.swift
index 2709b6e3c4e..55fb1455f17 100644
--- a/WireAPI/Tests/WireAPITests/Helpers/HTTPRequestSnapshotHelper.swift
+++ b/WireAPI/Tests/WireAPITests/Helpers/HTTPRequestSnapshotHelper.swift
@@ -40,17 +40,21 @@ struct HTTPRequestSnapshotHelper {
function: String = #function,
line: UInt = #line
) {
- let errorMessage = verifySnapshot(
- of: request,
- as: .dump,
- named: resourceName,
- file: file,
- testName: function,
- line: line
- )
+ let recordEnabled: SnapshotTestingConfiguration.Record? = ProcessInfo.processInfo
+ .environment["CI"] == "true" ? .never : nil
+ withSnapshotTesting(record: recordEnabled) {
+ let errorMessage = verifySnapshot(
+ of: request,
+ as: .dump,
+ named: resourceName,
+ file: file,
+ testName: function,
+ line: line
+ )
- if let errorMessage {
- XCTFail(errorMessage, file: file, line: line)
+ if let errorMessage {
+ XCTFail(errorMessage, file: file, line: line)
+ }
}
}
@@ -72,18 +76,22 @@ struct HTTPRequestSnapshotHelper {
function: String = #function,
line: UInt = #line
) {
- let errorMessage = verifySnapshot(
- of: request,
- as: .curl,
- named: resourceName,
- record: record,
- file: file,
- testName: function,
- line: line
- )
+ let recordEnabled: SnapshotTestingConfiguration.Record? = ProcessInfo.processInfo
+ .environment["CI"] == "true" ? .never : nil
+ withSnapshotTesting(record: recordEnabled) {
+ let errorMessage = verifySnapshot(
+ of: request,
+ as: .curl,
+ named: resourceName,
+ record: record,
+ file: file,
+ testName: function,
+ line: line
+ )
- if let errorMessage {
- XCTFail(errorMessage, file: file, line: line)
+ if let errorMessage {
+ XCTFail(errorMessage, file: file, line: line)
+ }
}
}
diff --git a/WireAPI/Tests/WireAPITests/Helpers/TestDataLoading.swift b/WireAPI/Tests/WireAPITests/Helpers/TestDataLoading.swift
new file mode 100644
index 00000000000..4f7d58e56db
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/Helpers/TestDataLoading.swift
@@ -0,0 +1,36 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+
+enum JSONLoadFailure: Error {
+ case resourceNotFound(fileName: String)
+}
+
+extension Decodable {
+
+ /// Instantiates `self` from a JSON file with the given `fileName`.
+ static func loadJSON(fileName: String) throws -> Self {
+ guard let url = Bundle.module.url(forResource: fileName, withExtension: "json") else {
+ throw JSONLoadFailure.resourceNotFound(fileName: fileName)
+ }
+ let data = try Data(contentsOf: url)
+ return try JSONDecoder().decode(Self.self, from: data)
+ }
+
+}
diff --git a/WireAPI/Tests/WireAPITests/Mocks/MockURLAuthenticationChallengeSender.swift b/WireAPI/Tests/WireAPITests/Mocks/MockURLAuthenticationChallengeSender.swift
new file mode 100644
index 00000000000..4035091d16c
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/Mocks/MockURLAuthenticationChallengeSender.swift
@@ -0,0 +1,30 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+
+@objc
+final class MockURLAuthenticationChallengeSender: NSObject, URLAuthenticationChallengeSender {
+
+ func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {}
+
+ func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {}
+
+ func cancel(_ challenge: URLAuthenticationChallenge) {}
+
+}
diff --git a/WireAPI/Sources/WireAPI/Network/PushChannel/PushChannelServiceError.swift b/WireAPI/Tests/WireAPITests/Mocks/MockURLProtectionSpace.swift
similarity index 79%
rename from WireAPI/Sources/WireAPI/Network/PushChannel/PushChannelServiceError.swift
rename to WireAPI/Tests/WireAPITests/Mocks/MockURLProtectionSpace.swift
index 2083c9bcfa0..3838899ba8b 100644
--- a/WireAPI/Sources/WireAPI/Network/PushChannel/PushChannelServiceError.swift
+++ b/WireAPI/Tests/WireAPITests/Mocks/MockURLProtectionSpace.swift
@@ -18,12 +18,11 @@
import Foundation
-/// Errors originating from `PushChannelService`.
+final class MockURLProtectionSpace: URLProtectionSpace, @unchecked Sendable {
-public enum PushChannelServiceError: Error {
-
- /// An access token is required but none is available.
-
- case missingAccessToken
+ var mockServerTrust: SecTrust?
+ override var serverTrust: SecTrust? {
+ mockServerTrust
+ }
}
diff --git a/wire-ios/Wire-iOS/Sources/Analytics/CountlyInstance.swift b/WireAPI/Tests/WireAPITests/Mocks/PublicKeys.swift
similarity index 72%
rename from wire-ios/Wire-iOS/Sources/Analytics/CountlyInstance.swift
rename to WireAPI/Tests/WireAPITests/Mocks/PublicKeys.swift
index d6c60d4964e..0be87b6a104 100644
--- a/wire-ios/Wire-iOS/Sources/Analytics/CountlyInstance.swift
+++ b/WireAPI/Tests/WireAPITests/Mocks/PublicKeys.swift
@@ -17,13 +17,16 @@
//
import Foundation
-import WireAnalytics
-protocol CountlyInstance {
- func recordEvent(_ key: String, segmentation: [String: String]?)
- func start(with config: WireCountlyConfig)
+struct PublicKeys: Decodable {
+ let wire: Data
+ let some: Data
- static func sharedInstance() -> Self
-}
+ static var wire: Data {
+ get throws { try loadJSON(fileName: "public-keys").wire }
+ }
-extension WireCountly: CountlyInstance {}
+ static var some: Data {
+ get throws { try loadJSON(fileName: "public-keys").some }
+ }
+}
diff --git a/WireAPI/Tests/WireAPITests/Mocks/SecTrust+Mock.swift b/WireAPI/Tests/WireAPITests/Mocks/SecTrust+Mock.swift
new file mode 100644
index 00000000000..12748cfb7ba
--- /dev/null
+++ b/WireAPI/Tests/WireAPITests/Mocks/SecTrust+Mock.swift
@@ -0,0 +1,69 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+
+extension SecTrust {
+
+ enum Failure: Error {
+ case failedToCreateCertificate
+ case failedToCreateTrust
+ }
+
+ static var wire: SecTrust {
+ get throws { try make(data: Certificates.load().wire) }
+ }
+
+ static var other: SecTrust {
+ get throws { try make(data: Certificates.load().other) }
+ }
+
+ static var invalid: SecTrust {
+ get throws { try make(data: Certificates.load().invalid) }
+ }
+
+ static func make(data: [Data]) throws -> SecTrust {
+ let policy = SecPolicyCreateBasicX509()
+
+ let certificates: [SecCertificate] = try data.compactMap {
+ guard let cert = SecCertificateCreateWithData(nil, $0 as CFData) else {
+ throw Failure.failedToCreateCertificate
+ }
+ return cert
+ }
+ var trust: SecTrust?
+ guard SecTrustCreateWithCertificates(certificates as CFTypeRef, policy, &trust) == 0, let result = trust else {
+ throw Failure.failedToCreateTrust
+ }
+
+ return result
+ }
+
+}
+
+private struct Certificates: Decodable {
+
+ let wire: [Data]
+ let other: [Data]
+ let invalid: [Data]
+
+ static func load() throws -> Certificates {
+ try Certificates.loadJSON(fileName: "certificates")
+ }
+
+}
diff --git a/WireAPI/Tests/WireAPITests/Network/APIService/APIServiceTests.swift b/WireAPI/Tests/WireAPITests/Network/APIService/APIServiceTests.swift
index 6784e30da01..82cbac75133 100644
--- a/WireAPI/Tests/WireAPITests/Network/APIService/APIServiceTests.swift
+++ b/WireAPI/Tests/WireAPITests/Network/APIService/APIServiceTests.swift
@@ -31,7 +31,10 @@ final class APIServiceTests: XCTestCase {
override func setUp() async throws {
backendURL = try XCTUnwrap(URL(string: "https://www.example.com"))
authenticationManager = MockAuthenticationManagerProtocol()
- let networkService = NetworkService(baseURL: backendURL)
+ let networkService = NetworkService(
+ baseURL: backendURL,
+ serverTrustValidator: ServerTrustValidator(pinnedKeys: [])
+ )
networkService.configure(with: .mockURLSession())
sut = APIService(
networkService: networkService,
diff --git a/WireAPI/Tests/WireAPITests/Network/NetworkService/NetworkServiceTests.swift b/WireAPI/Tests/WireAPITests/Network/NetworkService/NetworkServiceTests.swift
index 3856a8fc0d6..442fef63976 100644
--- a/WireAPI/Tests/WireAPITests/Network/NetworkService/NetworkServiceTests.swift
+++ b/WireAPI/Tests/WireAPITests/Network/NetworkService/NetworkServiceTests.swift
@@ -24,17 +24,25 @@ import XCTest
final class NetworkServiceTests: XCTestCase {
+ var session: URLSession!
var sut: NetworkService!
var backendURL: URL!
override func setUp() async throws {
try await super.setUp()
+ session = .mockURLSession()
backendURL = try XCTUnwrap(URL(string: "https://www.example.com"))
- sut = NetworkService(baseURL: backendURL)
- sut.configure(with: .mockURLSession())
+ sut = NetworkService(
+ baseURL: backendURL,
+ serverTrustValidator: ServerTrustValidator(pinnedKeys: [
+ try PinnedKey(key: PublicKeys.wire, hosts: [.equals("prod-nginz-https.wire.com")])
+ ])
+ )
+ sut.configure(with: session)
}
override func tearDown() async throws {
+ session = nil
backendURL = nil
sut = nil
try await super.tearDown()
@@ -91,6 +99,66 @@ final class NetworkServiceTests: XCTestCase {
XCTAssertEqual(receivedRequest.url?.absoluteString, backendURL.appendingPathComponent("/foo").absoluteString)
}
+ // MARK: - URLAuthenticationChallenge
+
+ func testUserSessionDidReceiveChallenge_whenNotServerTrustAuthenticationMethod() async throws {
+ let methods = [
+ NSURLAuthenticationMethodClientCertificate,
+ NSURLAuthenticationMethodNegotiate,
+ NSURLAuthenticationMethodNTLM,
+ NSURLAuthenticationMethodHTMLForm,
+ NSURLAuthenticationMethodHTTPDigest,
+ NSURLAuthenticationMethodHTTPBasic,
+ NSURLAuthenticationMethodDefault
+ ]
+
+ for method in methods {
+ // Given
+ let challenge = try Scaffolding.makeAuthenticationChallenge(
+ authenticationMethod: method,
+ serverTrust: .invalid
+ )
+ let task = session.dataTask(with: Scaffolding.getRequest)
+
+ // When
+ let result = await sut.urlSession(session, task: task, didReceive: challenge)
+
+ // Then
+ XCTAssertEqual(result.0, .performDefaultHandling)
+ XCTAssertEqual(result.1, Scaffolding.makeCredential())
+ }
+ }
+
+ func testUserSessionDidReceiveChallenge_whenServerTrustValid() async throws {
+ let challenge = try Scaffolding.makeAuthenticationChallenge(
+ authenticationMethod: NSURLAuthenticationMethodServerTrust,
+ serverTrust: .wire
+ )
+ let task = session.dataTask(with: Scaffolding.getRequest)
+
+ // When
+ let result = await sut.urlSession(session, task: task, didReceive: challenge)
+
+ // Then
+ XCTAssertEqual(result.0, .performDefaultHandling)
+ XCTAssertEqual(result.1, Scaffolding.makeCredential())
+ }
+
+ func testUserSessionDidReceiveChallenge_whenServerTrustInvalid() async throws {
+ let challenge = try Scaffolding.makeAuthenticationChallenge(
+ authenticationMethod: NSURLAuthenticationMethodServerTrust,
+ serverTrust: .invalid
+ )
+ let task = session.dataTask(with: Scaffolding.getRequest)
+
+ // When
+ let result = await sut.urlSession(session, task: task, didReceive: challenge)
+
+ // Then
+ XCTAssertEqual(result.0, .cancelAuthenticationChallenge)
+ XCTAssertNil(result.1)
+ }
+
}
private enum Scaffolding {
@@ -106,4 +174,31 @@ private enum Scaffolding {
return request
}()
+ static func makeCredential() -> URLCredential {
+ URLCredential(user: "user", password: "password", persistence: .none)
+ }
+
+ static func makeAuthenticationChallenge(
+ authenticationMethod: String,
+ serverTrust: SecTrust
+ ) throws -> URLAuthenticationChallenge {
+ let protectionSpace = MockURLProtectionSpace(
+ host: "prod-nginz-https.wire.com",
+ port: 8080,
+ protocol: nil,
+ realm: nil,
+ authenticationMethod: authenticationMethod
+ )
+
+ protectionSpace.mockServerTrust = serverTrust
+
+ return URLAuthenticationChallenge(
+ protectionSpace: protectionSpace,
+ proposedCredential: Scaffolding.makeCredential(),
+ previousFailureCount: 0,
+ failureResponse: nil,
+ error: nil,
+ sender: MockURLAuthenticationChallengeSender()
+ )
+ }
}
diff --git a/WireAPI/Tests/WireAPITests/Network/PushChannel/PushChannelTests.swift b/WireAPI/Tests/WireAPITests/Network/PushChannel/PushChannelTests.swift
index 5fddc329a03..f9315205895 100644
--- a/WireAPI/Tests/WireAPITests/Network/PushChannel/PushChannelTests.swift
+++ b/WireAPI/Tests/WireAPITests/Network/PushChannel/PushChannelTests.swift
@@ -201,7 +201,7 @@ private enum Scaffolding {
static let receiptModeUpdateEvent = ConversationReceiptModeUpdateEvent(
conversationID: conversationID,
senderID: senderID,
- newRecieptMode: 1
+ newReceiptMode: 1
)
static let renameEvent = ConversationRenameEvent(
diff --git a/WireAPI/Tests/WireAPITests/UpdateEvent/ConversationEventDecodingTests.swift b/WireAPI/Tests/WireAPITests/UpdateEvent/ConversationEventDecodingTests.swift
index ac51a5a235e..867a57b9202 100644
--- a/WireAPI/Tests/WireAPITests/UpdateEvent/ConversationEventDecodingTests.swift
+++ b/WireAPI/Tests/WireAPITests/UpdateEvent/ConversationEventDecodingTests.swift
@@ -486,7 +486,7 @@ final class ConversationEventDecodingTests: XCTestCase {
static let receiptModeUpdateEvent = ConversationReceiptModeUpdateEvent(
conversationID: conversationID,
senderID: senderID,
- newRecieptMode: 1
+ newReceiptMode: 1
)
static let renameEvent = ConversationRenameEvent(
diff --git a/WireAnalytics/.swiftpm/WireAnalytics.xctestplan b/WireAnalytics/.swiftpm/WireAnalytics.xctestplan
deleted file mode 100644
index 4c9af146b50..00000000000
--- a/WireAnalytics/.swiftpm/WireAnalytics.xctestplan
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "configurations" : [
- {
- "id" : "1AD1FA32-0D55-4D7B-9E4B-C64035D0D3B3",
- "name" : "Test Scheme Action",
- "options" : {
-
- }
- }
- ],
- "defaultOptions" : {
- "codeCoverage" : true
- },
- "testTargets" : [
- {
- "target" : {
- "containerPath" : "container:",
- "identifier" : "WireAnalyticsTests",
- "name" : "WireAnalyticsTests"
- }
- }
- ],
- "version" : 1
-}
diff --git a/WireAnalytics/.swiftpm/xcode/xcshareddata/xcschemes/WireDatadog.xcscheme b/WireAnalytics/.swiftpm/xcode/xcshareddata/xcschemes/WireDatadog.xcscheme
deleted file mode 100644
index 704e826360f..00000000000
--- a/WireAnalytics/.swiftpm/xcode/xcshareddata/xcschemes/WireDatadog.xcscheme
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/WireAnalytics/Package.swift b/WireAnalytics/Package.swift
index 27c2bca8bfc..3d0545e1475 100644
--- a/WireAnalytics/Package.swift
+++ b/WireAnalytics/Package.swift
@@ -1,56 +1,88 @@
-// swift-tools-version: 5.10
+// swift-tools-version: 6.0
import Foundation
import PackageDescription
// You can enable/disable Datadog for debugging by overriding the boolean.
-let datadogEnabled = hasEnvironmentVariable("ENABLE_DATADOG", "true")
+let isDatadogEnabled = hasEnvironmentVariable("ENABLE_DATADOG", "true")
+let isCountlyEnabled = hasEnvironmentVariable("ENABLE_COUNTLY", "true")
let package = Package(
name: "WireAnalytics",
platforms: [.iOS(.v16), .macOS(.v12)],
products: [
.library(name: "WireAnalytics", targets: ["WireAnalytics"]),
- .library(name: "WireDatadog", targets: ["WireDatadog"]),
- .library(name: "WireAnalyticsSupport", targets: ["WireAnalyticsSupport"])
+ .library(name: "WireAnalyticsSupport", targets: ["WireAnalyticsSupport"]),
+ .library(name: "WireCountly", targets: ["WireCountly"]),
+ .library(name: "WireDatadog", targets: ["WireDatadog"])
],
dependencies: [
- .package(url: "https://github.com/DataDog/dd-sdk-ios.git", exact: "2.18.0"),
.package(url: "https://github.com/Countly/countly-sdk-ios.git", exact: "24.4.2"),
+ .package(url: "https://github.com/DataDog/dd-sdk-ios.git", exact: "2.18.0"),
+ .package(path: "../WireLogging"),
.package(path: "../WirePlugins")
],
targets: [
.target(
name: "WireAnalytics",
- dependencies: [
- .product(name: "Countly", package: "countly-sdk-ios")
- ],
- swiftSettings: swiftSettings
- ),
- .target(
- name: "WireDatadog",
- dependencies: datadogDependencies(),
- path: "Sources/WireDatadog",
- sources: datadogFiles(),
- swiftSettings: swiftSettings
+ dependencies: ["WireLogging"]
),
.target(
name: "WireAnalyticsSupport",
dependencies: ["WireAnalytics"],
- plugins: [
- .plugin(name: "SourceryPlugin", package: "WirePlugins")
- ]
+ plugins: [.plugin(name: "SourceryPlugin", package: "WirePlugins")]
),
.testTarget(
name: "WireAnalyticsTests",
dependencies: ["WireAnalytics", "WireAnalyticsSupport"]
+ ),
+
+ .target(
+ name: "WireCountly",
+ dependencies: countlyDependencies() + ["WireAnalytics"],
+ sources: countlyFiles()
+ ),
+
+ .target(
+ name: "WireDatadog",
+ dependencies: datadogDependencies() + ["WireLogging"],
+ sources: datadogFiles()
+ ),
+
+ // This target prevents warnings about `countly-sdk-ios` or `dd-sdk-ios` not being used.
+ .target(
+ name: "WarningPrevention",
+ dependencies: [
+ .product(name: "Countly", package: "countly-sdk-ios"),
+ .product(name: "DatadogCore", package: "dd-sdk-ios")
+ ]
)
]
)
+// MARK: - Countly
+
+func countlyDependencies() -> [Target.Dependency] {
+ guard isCountlyEnabled else {
+ return []
+ }
+ return [
+ .product(name: "Countly", package: "countly-sdk-ios")
+ ]
+}
+
+func countlyFiles() -> [String] {
+ if isCountlyEnabled {
+ ["CountlyWrapper.swift"]
+ } else {
+ ["CountlyDummy.swift"]
+ }
+}
+
+// MARK: - Datadog
+
func datadogDependencies() -> [Target.Dependency] {
- guard datadogEnabled else {
- // note: in this case SPM will warn that the dd-sdk-ios is not used
+ guard isDatadogEnabled else {
return []
}
return [
@@ -63,13 +95,15 @@ func datadogDependencies() -> [Target.Dependency] {
}
func datadogFiles() -> [String] {
- if datadogEnabled {
- ["WireDatadog.swift", "LogLevel.swift"]
+ if isDatadogEnabled {
+ ["WireDatadog.swift"]
} else {
- ["WireFakeDatadog.swift", "LogLevel.swift"]
+ ["WireFakeDatadog.swift"]
}
}
+// MARK: -
+
func hasEnvironmentVariable(_ name: String, _ value: String? = nil) -> Bool {
if let value {
ProcessInfo.processInfo.environment[name] == value
@@ -78,8 +112,10 @@ func hasEnvironmentVariable(_ name: String, _ value: String? = nil) -> Bool {
}
}
-let swiftSettings: [SwiftSetting] = [
- .enableUpcomingFeature("ExistentialAny"),
- .enableUpcomingFeature("GlobalConcurrency"),
- .enableExperimentalFeature("StrictConcurrency")
-]
+for target in package.targets {
+ target.swiftSettings = (target.swiftSettings ?? []) + [
+ .enableUpcomingFeature("InternalImportsByDefault"),
+ .enableUpcomingFeature("FullTypedThrows"),
+ .enableUpcomingFeature("ExistentialAny")
+ ]
+}
diff --git a/WireAnalytics/Sources/WarningPrevention/WarningPrevention.swift b/WireAnalytics/Sources/WarningPrevention/WarningPrevention.swift
new file mode 100644
index 00000000000..b92d3a9c94c
--- /dev/null
+++ b/WireAnalytics/Sources/WarningPrevention/WarningPrevention.swift
@@ -0,0 +1,22 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+// A signle empty file needed for the `WarningPrevention` target.
+// The target is not to be referenced, its purpose is only to not trigger
+// warnings about unused dependencies when Countly or Datadog are not
+// enabled via environment variables.
diff --git a/WireAnalytics/Sources/WireAnalytics/Countly/CountlyProtocol.swift b/WireAnalytics/Sources/WireAnalytics/Countly/CountlyProtocol.swift
index eb26be7daba..e6d758e1099 100644
--- a/WireAnalytics/Sources/WireAnalytics/Countly/CountlyProtocol.swift
+++ b/WireAnalytics/Sources/WireAnalytics/Countly/CountlyProtocol.swift
@@ -16,11 +16,11 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import Foundation
+public import Foundation
// sourcery: AutoMockable
-/// Abstraction around `Countly`.
-protocol CountlyProtocol {
+/// Mirrors the API of `Countly`.
+public protocol CountlyProtocol {
func resetInstance()
diff --git a/WireAnalytics/Sources/WireAnalytics/Events/CallQualitySurveyReview.swift b/WireAnalytics/Sources/WireAnalytics/Events/CallQualitySurveyReview.swift
index d0992460bb4..c09d7bf7bcd 100644
--- a/WireAnalytics/Sources/WireAnalytics/Events/CallQualitySurveyReview.swift
+++ b/WireAnalytics/Sources/WireAnalytics/Events/CallQualitySurveyReview.swift
@@ -16,7 +16,7 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import Foundation
+public import Foundation
/// User review for call quality.
diff --git a/WireAnalytics/Sources/WireAnalytics/Service/AnalyticsEventTracker.swift b/WireAnalytics/Sources/WireAnalytics/Service/AnalyticsEventTracker.swift
index b7246be0d9b..97e8ded51c6 100644
--- a/WireAnalytics/Sources/WireAnalytics/Service/AnalyticsEventTracker.swift
+++ b/WireAnalytics/Sources/WireAnalytics/Service/AnalyticsEventTracker.swift
@@ -16,8 +16,6 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import Foundation
-
// sourcery: AutoMockable
/// An object that tracks analytic events.
public protocol AnalyticsEventTracker: AnyObject {
diff --git a/WireAnalytics/Sources/WireAnalytics/Service/AnalyticsService.swift b/WireAnalytics/Sources/WireAnalytics/Service/AnalyticsService.swift
index 5d5bbc38676..fa28bea45bc 100644
--- a/WireAnalytics/Sources/WireAnalytics/Service/AnalyticsService.swift
+++ b/WireAnalytics/Sources/WireAnalytics/Service/AnalyticsService.swift
@@ -16,8 +16,9 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import Countly
-import Foundation
+public import Foundation
+
+import WireLogging
/// A service for tracking analytics data generated by the user.
@@ -30,43 +31,40 @@ public final class AnalyticsService: AnalyticsServiceProtocol {
private var countly: (any CountlyProtocol)?
private var currentUser: AnalyticsUser?
private let baseSegmentation: Set
- private let logger: (String) -> Void
+ private let logger: WireLogger
// MARK: - Life cycle
/// Create a new `AnalyticsService`.
///
/// A non-nil config is needed to track analytics data.
- ///
- /// - Parameters:
- /// - config: A config containing the authentication key and server host.
- /// - logger: A function that logs a message on behalf of the service.
public convenience init(
config: Config?,
- logger: @escaping (String) -> Void
+ deviceModel: String,
+ deviceOS: String,
+ countlyProvider: @escaping () -> any CountlyProtocol
) {
self.init(
config: config,
baseSegmentation: [
- .deviceModel(UIDevice.current.model),
- .deviceOS(UIDevice.current.systemVersion)
+ .deviceModel(deviceModel),
+ .deviceOS(deviceOS)
],
- logger: logger,
- countlyProvider: { Countly.sharedInstance() }
+ countlyProvider: countlyProvider
)
}
init(
config: Config?,
baseSegmentation: Set,
- logger: @escaping (String) -> Void,
countlyProvider: @escaping () -> any CountlyProtocol
) {
self.config = config
self.baseSegmentation = baseSegmentation
- self.logger = logger
self.countlyProvider = countlyProvider
+
+ self.logger = .analytics
}
// MARK: - Enable / disable
@@ -85,23 +83,22 @@ public final class AnalyticsService: AnalyticsServiceProtocol {
/// Start sending analytics data.
+ @MainActor
public func enableTracking() async throws {
guard let config else {
throw AnalyticsServiceError.serviceIsNotConfigured
}
- logger("enabling tracking")
+ logger.debug("enabling tracking")
let countly = countlyProvider()
self.countly = countly
// Countly invokes UI apis so we need to run it on the main thread.
- await MainActor.run {
- countly.start(
- appKey: config.secretKey,
- host: config.serverHost
- )
- }
+ countly.start(
+ appKey: config.secretKey,
+ host: config.serverHost
+ )
}
/// Stop sending analytics data.
@@ -111,7 +108,7 @@ public final class AnalyticsService: AnalyticsServiceProtocol {
throw AnalyticsServiceError.serviceIsNotConfigured
}
- logger("disabling tracking")
+ logger.debug("disabling tracking")
countly.endSession()
try clearCurrentUser()
@@ -134,7 +131,7 @@ public final class AnalyticsService: AnalyticsServiceProtocol {
return
}
- logger("switching user")
+ logger.debug("switching user")
countly.endSession()
@@ -161,7 +158,7 @@ public final class AnalyticsService: AnalyticsServiceProtocol {
return
}
- logger("updating current user")
+ logger.debug("updating current user")
try pushUser(
user,
@@ -203,7 +200,7 @@ public final class AnalyticsService: AnalyticsServiceProtocol {
}
private func clearCurrentUser() throws {
- logger("clearing current user")
+ logger.debug("clearing current user")
try pushUser(
nil,
@@ -232,7 +229,7 @@ public final class AnalyticsService: AnalyticsServiceProtocol {
($0.key, $0.value)
})
- logger("tracking event: \(event)")
+ logger.debug("tracking event: \(event)")
countly.recordEvent(
event.name,
diff --git a/WireAnalytics/Sources/WireAnalytics/Service/AnalyticsServiceProtocol.swift b/WireAnalytics/Sources/WireAnalytics/Service/AnalyticsServiceProtocol.swift
index 8205d41de37..074ef2db0a3 100644
--- a/WireAnalytics/Sources/WireAnalytics/Service/AnalyticsServiceProtocol.swift
+++ b/WireAnalytics/Sources/WireAnalytics/Service/AnalyticsServiceProtocol.swift
@@ -16,8 +16,6 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import Foundation
-
/// A service for tracking analytics data generated by the user.
public protocol AnalyticsServiceProtocol: AnalyticsEventTracker {
diff --git a/WireAnalytics/Sources/WireAnalyticsSupport/MockAnalyticsServiceProtocol.swift b/WireAnalytics/Sources/WireAnalyticsSupport/MockAnalyticsServiceProtocol.swift
index ec76b3420b1..f5c97fbf980 100644
--- a/WireAnalytics/Sources/WireAnalyticsSupport/MockAnalyticsServiceProtocol.swift
+++ b/WireAnalytics/Sources/WireAnalyticsSupport/MockAnalyticsServiceProtocol.swift
@@ -15,7 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import WireAnalytics
+
+public import WireAnalytics
// this mock is generated manually because of (any Error)?
// TODO: [WPB-11829] update sourcery
diff --git a/WireAnalytics/Sources/WireAnalyticsSupport/Sourcery/sourcery.yml b/WireAnalytics/Sources/WireAnalyticsSupport/Sourcery/sourcery.yml
index 6dfea6d2606..b53b65b8305 100644
--- a/WireAnalytics/Sources/WireAnalyticsSupport/Sourcery/sourcery.yml
+++ b/WireAnalytics/Sources/WireAnalyticsSupport/Sourcery/sourcery.yml
@@ -3,6 +3,6 @@ sources:
templates:
- ${TARGET_DIR}/Sourcery/AutoMockable.stencil
output:
- ${DERIVED_SOURCES_DIR}
+ ${DERIVED_SOURCES_DIR}
args:
- autoMockableTestableImports: ["WireAnalytics"]
+ autoMockablePublicImports: ["Foundation", "WireAnalytics"]
diff --git a/WireAnalytics/Sources/WireCountly/CountlyDummy.swift b/WireAnalytics/Sources/WireCountly/CountlyDummy.swift
new file mode 100644
index 00000000000..44746ba92fd
--- /dev/null
+++ b/WireAnalytics/Sources/WireCountly/CountlyDummy.swift
@@ -0,0 +1,35 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+public import Foundation
+public import WireAnalytics
+
+public typealias CountlyWrapper = CountlyDummy
+
+public struct CountlyDummy: CountlyProtocol {
+
+ public init() {}
+
+ public func resetInstance() {}
+ public func start(appKey: String, host: URL) {}
+ public func setUserValue(_ value: String?, forKey key: String) {}
+ public func changeDeviceID(_ id: String, mergeData: Bool) {}
+ public func beginSession() {}
+ public func endSession() {}
+ public func recordEvent(_ key: String, segmentation: [String: String]?) {}
+}
diff --git a/WireAnalytics/Sources/WireAnalytics/Countly/Countly+CountlyProtocol.swift b/WireAnalytics/Sources/WireCountly/CountlyWrapper.swift
similarity index 57%
rename from WireAnalytics/Sources/WireAnalytics/Countly/Countly+CountlyProtocol.swift
rename to WireAnalytics/Sources/WireCountly/CountlyWrapper.swift
index e3c360b6f64..50ded49417e 100644
--- a/WireAnalytics/Sources/WireAnalytics/Countly/Countly+CountlyProtocol.swift
+++ b/WireAnalytics/Sources/WireCountly/CountlyWrapper.swift
@@ -16,10 +16,23 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import Countly
+public import Foundation
+public import WireAnalytics
+public import Countly
-extension Countly: CountlyProtocol {
- func start(
+public struct CountlyWrapper: CountlyProtocol {
+
+ var countly: () -> Countly
+
+ public init(countly: @escaping @autoclosure () -> Countly = .sharedInstance()) {
+ self.countly = countly
+ }
+
+ public func resetInstance() {
+ countly().resetInstance()
+ }
+
+ public func start(
appKey: String,
host: URL
) {
@@ -27,29 +40,44 @@ extension Countly: CountlyProtocol {
config.appKey = appKey
config.manualSessionHandling = true
config.host = host.absoluteString
- start(with: config)
+ countly().start(with: config)
}
- func changeDeviceID(
+ public func setUserValue(
+ _ value: String?,
+ forKey key: String
+ ) {
+ if let value {
+ Countly.user().set(key, value: value)
+ } else {
+ Countly.user().unSet(key)
+ }
+ }
+
+ public func changeDeviceID(
_ id: String,
mergeData: Bool
) {
if mergeData {
- changeDeviceID(withMerge: id)
+ countly().changeDeviceID(withMerge: id)
} else {
- changeDeviceIDWithoutMerge(id)
+ countly().changeDeviceIDWithoutMerge(id)
}
}
- func setUserValue(
- _ value: String?,
- forKey key: String
+ public func beginSession() {
+ countly().beginSession()
+ }
+
+ public func endSession() {
+ countly().endSession()
+ }
+
+ public func recordEvent(
+ _ key: String,
+ segmentation: [String: String]?
) {
- if let value {
- Countly.user().set(key, value: value)
- } else {
- Countly.user().unSet(key)
- }
+ countly().recordEvent(key, segmentation: segmentation)
}
}
diff --git a/WireAnalytics/Sources/WireCountly/PrivacyInfo.xcprivacy b/WireAnalytics/Sources/WireCountly/PrivacyInfo.xcprivacy
new file mode 120000
index 00000000000..11b855aa14b
--- /dev/null
+++ b/WireAnalytics/Sources/WireCountly/PrivacyInfo.xcprivacy
@@ -0,0 +1 @@
+../../.PrivacyInfo.xcprivacy
\ No newline at end of file
diff --git a/WireAnalytics/Sources/WireDatadog/WireDatadog.swift b/WireAnalytics/Sources/WireDatadog/WireDatadog.swift
index b21fad0da73..627fc2a5734 100644
--- a/WireAnalytics/Sources/WireDatadog/WireDatadog.swift
+++ b/WireAnalytics/Sources/WireDatadog/WireDatadog.swift
@@ -16,6 +16,9 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
+public import Foundation
+public import WireLogging
+
import CryptoKit
import DatadogCore
import DatadogCrashReporting
@@ -29,7 +32,7 @@ public final class WireDatadog {
private let applicationID: String
private let buildVersion: String
private let buildNumber: String
- private let logLevel: WireDatadog.LogLevel = .debug
+ private let logLevel: WireLogLevel = .debug
public private(set) var userIdentifier: String
private(set) var logger: (any DatadogLogs.LoggerProtocol)?
@@ -72,7 +75,7 @@ public final class WireDatadog {
let loggerConfiguration = Logger.Configuration(
name: "iOS Wire App",
networkInfoEnabled: true,
- remoteLogThreshold: logLevel.datadogLevel
+ remoteLogThreshold: logLevel.mapToDatadogLogLevel()
)
self.logger = Logger.create(with: loggerConfiguration)
}
@@ -96,7 +99,7 @@ public final class WireDatadog {
Datadog.setUserInfo(id: userIdentifier)
logger?.log(
- level: logLevel.datadogLevel,
+ level: logLevel.mapToDatadogLogLevel(),
message: "Datadog startMonitoring for device: \(userIdentifier)",
error: nil,
attributes: nil
@@ -104,7 +107,7 @@ public final class WireDatadog {
}
public func log(
- level: WireDatadog.LogLevel,
+ level: WireLogLevel,
message: String,
error: (any Error)? = nil,
attributes: [String: any Encodable]
@@ -113,8 +116,10 @@ public final class WireDatadog {
finalAttributes["build_number"] = buildNumber
finalAttributes["version"] = buildVersion
+ let level = level.mapToDatadogLogLevel()
+
logger?.log(
- level: level.datadogLevel, // TODO: [WPB-11881] review this when WireLogger is available as package
+ level: level,
message: message,
error: error,
attributes: finalAttributes
@@ -148,8 +153,8 @@ public final class WireDatadog {
}
}
-extension WireDatadog.LogLevel {
- var datadogLevel: LogLevel {
+extension WireLogLevel {
+ func mapToDatadogLogLevel() -> LogLevel {
switch self {
case .debug:
.debug
diff --git a/WireAnalytics/Sources/WireDatadog/WireFakeDatadog.swift b/WireAnalytics/Sources/WireDatadog/WireFakeDatadog.swift
index 61e7a2c57a3..613aebf8035 100644
--- a/WireAnalytics/Sources/WireDatadog/WireFakeDatadog.swift
+++ b/WireAnalytics/Sources/WireDatadog/WireFakeDatadog.swift
@@ -16,7 +16,8 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import UIKit
+public import Foundation
+public import WireLogging
public final class WireDatadog {
@@ -40,7 +41,7 @@ public final class WireDatadog {
}
public func log(
- level: WireDatadog.LogLevel,
+ level: WireLogLevel,
message: String,
error: (any Error)? = nil,
attributes: [String: any Encodable]
diff --git a/WireUI/.swiftpm/WireAccountImageUI.xctestplan b/WireAnalytics/Tests/TestPlans/AllTests.xctestplan
similarity index 54%
rename from WireUI/.swiftpm/WireAccountImageUI.xctestplan
rename to WireAnalytics/Tests/TestPlans/AllTests.xctestplan
index 741557b9235..1ce51f179d1 100644
--- a/WireUI/.swiftpm/WireAccountImageUI.xctestplan
+++ b/WireAnalytics/Tests/TestPlans/AllTests.xctestplan
@@ -1,8 +1,8 @@
{
"configurations" : [
{
- "id" : "EE19C776-C34B-4ED9-B55F-54761C5D7C65",
- "name" : "Test Scheme Action",
+ "id" : "BE2F1BC8-EAC1-496B-9F9E-FD0251975803",
+ "name" : "Configuration 1",
"options" : {
}
@@ -16,9 +16,9 @@
"testTargets" : [
{
"target" : {
- "containerPath" : "container:",
- "identifier" : "WireAccountImageUITests",
- "name" : "WireAccountImageUITests"
+ "containerPath" : "container:WireAnalytics",
+ "identifier" : "WireAnalyticsTests",
+ "name" : "WireAnalyticsTests"
}
}
],
diff --git a/WireAnalytics/Tests/WireAnalyticsTests/AnalyticsServiceTests.swift b/WireAnalytics/Tests/WireAnalyticsTests/AnalyticsServiceTests.swift
index b5d7916f99e..a990e1e9a0e 100644
--- a/WireAnalytics/Tests/WireAnalyticsTests/AnalyticsServiceTests.swift
+++ b/WireAnalytics/Tests/WireAnalyticsTests/AnalyticsServiceTests.swift
@@ -24,49 +24,48 @@ import XCTest
class AnalyticsServiceTests: XCTestCase {
private var sut: AnalyticsService!
- private var countly: MockCountlyProtocol!
+ private var countlyMock: MockCountlyProtocol!
override func setUpWithError() throws {
- try super.setUpWithError()
- countly = MockCountlyProtocol()
+ countlyMock = .init()
sut = AnalyticsService(
config: Scaffolding.config,
baseSegmentation: Scaffolding.baseSegmentation,
- logger: { print($0) },
- countlyProvider: { self.countly }
+ countlyProvider: { self.countlyMock }
)
- countly.startAppKeyHost_MockMethod = { _, _ in }
- countly.resetInstance_MockMethod = {}
- countly.endSession_MockMethod = {}
- countly.beginSession_MockMethod = {}
- countly.changeDeviceIDMergeData_MockMethod = { _, _ in }
- countly.setUserValueForKey_MockMethod = { _, _ in }
- countly.recordEventSegmentation_MockMethod = { _, _ in }
+ countlyMock.startAppKeyHost_MockMethod = { _, _ in }
+ countlyMock.resetInstance_MockMethod = {}
+ countlyMock.endSession_MockMethod = {}
+ countlyMock.beginSession_MockMethod = {}
+ countlyMock.changeDeviceIDMergeData_MockMethod = { _, _ in }
+ countlyMock.setUserValueForKey_MockMethod = { _, _ in }
+ countlyMock.recordEventSegmentation_MockMethod = { _, _ in }
}
override func tearDown() {
- countly = nil
+ countlyMock = nil
sut = nil
- super.tearDown()
}
func resetMockInvocations() {
- countly.startAppKeyHost_Invocations = []
- countly.endSession_Invocations = []
- countly.beginSession_Invocations = []
- countly.changeDeviceIDMergeData_Invocations = []
- countly.setUserValueForKey_Invocations = []
- countly.resetInstance_Invocations = []
+ countlyMock.startAppKeyHost_Invocations = []
+ countlyMock.endSession_Invocations = []
+ countlyMock.beginSession_Invocations = []
+ countlyMock.changeDeviceIDMergeData_Invocations = []
+ countlyMock.setUserValueForKey_Invocations = []
+ countlyMock.resetInstance_Invocations = []
}
// MARK: - Tests
+ @MainActor
func testEnableTracking_service_is_not_configured() async throws {
// Given a service with no config.
let sut = AnalyticsService(
config: nil,
- logger: { print($0) }
+ baseSegmentation: Scaffolding.baseSegmentation,
+ countlyProvider: { self.countlyMock }
)
do {
@@ -78,12 +77,13 @@ class AnalyticsServiceTests: XCTestCase {
}
}
+ @MainActor
func testEnableTracking_succeeds() async throws {
// When tracking is enabled.
try await sut.enableTracking()
// Then the service was started.
- let invocations = countly.startAppKeyHost_Invocations
+ let invocations = countlyMock.startAppKeyHost_Invocations
guard invocations.count == 1 else {
XCTFail("expected 1 invocation, got: \(invocations.count)")
@@ -94,7 +94,7 @@ class AnalyticsServiceTests: XCTestCase {
XCTAssertEqual(invocations[0].host, Scaffolding.config.serverHost)
// Then no session has started yet.
- XCTAssertEqual(countly.beginSession_Invocations.count, 0)
+ XCTAssertEqual(countlyMock.beginSession_Invocations.count, 0)
}
func testDisableTracking_service_is_not_configured() throws {
@@ -110,6 +110,7 @@ class AnalyticsServiceTests: XCTestCase {
}
}
+ @MainActor
func testDisableTracking_succeeds() async throws {
// Given tracking is enabled.
try await sut.enableTracking()
@@ -119,11 +120,11 @@ class AnalyticsServiceTests: XCTestCase {
try sut.disableTracking()
// Then any session was ended and the service was reset.
- XCTAssertEqual(countly.endSession_Invocations.count, 1)
- XCTAssertEqual(countly.resetInstance_Invocations.count, 1)
+ XCTAssertEqual(countlyMock.endSession_Invocations.count, 1)
+ XCTAssertEqual(countlyMock.resetInstance_Invocations.count, 1)
// Then the user was cleared.
- let setUserInvocations = countly.setUserValueForKey_Invocations
+ let setUserInvocations = countlyMock.setUserValueForKey_Invocations
guard setUserInvocations.count == 3 else {
XCTFail("expected 3 invocation, got: \(setUserInvocations.count)")
@@ -150,6 +151,7 @@ class AnalyticsServiceTests: XCTestCase {
}
}
+ @MainActor
func testSwitchUser_user_is_same() async throws {
// Given tracking is enabled.
try await sut.enableTracking()
@@ -162,12 +164,13 @@ class AnalyticsServiceTests: XCTestCase {
try sut.switchUser(Scaffolding.user)
// Then the user was not switched again.
- XCTAssertEqual(countly.endSession_Invocations.count, 0)
- XCTAssertEqual(countly.changeDeviceIDMergeData_Invocations.count, 0)
- XCTAssertEqual(countly.setUserValueForKey_Invocations.count, 0)
- XCTAssertEqual(countly.beginSession_Invocations.count, 0)
+ XCTAssertEqual(countlyMock.endSession_Invocations.count, 0)
+ XCTAssertEqual(countlyMock.changeDeviceIDMergeData_Invocations.count, 0)
+ XCTAssertEqual(countlyMock.setUserValueForKey_Invocations.count, 0)
+ XCTAssertEqual(countlyMock.beginSession_Invocations.count, 0)
}
+ @MainActor
func testSwitchUser_succeeds() async throws {
// Given tracking is enabled.
try await sut.enableTracking()
@@ -180,10 +183,10 @@ class AnalyticsServiceTests: XCTestCase {
try sut.switchUser(Scaffolding.userWithTeam)
// Then the existing session was ended.
- XCTAssertEqual(countly.endSession_Invocations.count, 1)
+ XCTAssertEqual(countlyMock.endSession_Invocations.count, 1)
// Then the device id was changed.
- let deviceChangeInvocations = countly.changeDeviceIDMergeData_Invocations
+ let deviceChangeInvocations = countlyMock.changeDeviceIDMergeData_Invocations
guard deviceChangeInvocations.count == 1 else {
XCTFail("expected 1 device change invocation, got \(deviceChangeInvocations.count)")
return
@@ -193,7 +196,7 @@ class AnalyticsServiceTests: XCTestCase {
XCTAssertEqual(deviceChangeInvocations[0].mergeData, false)
// Then the user details were set.
- let userSetInvocations = countly.setUserValueForKey_Invocations
+ let userSetInvocations = countlyMock.setUserValueForKey_Invocations
guard userSetInvocations.count == 3 else {
XCTFail("expected 3 user set invocations, got \(userSetInvocations.count)")
return
@@ -208,9 +211,10 @@ class AnalyticsServiceTests: XCTestCase {
XCTAssertEqual(userSetInvocations[2].value, String(teamInfo.size.logRound()))
// Then a new session was started.
- XCTAssertEqual(countly.beginSession_Invocations.count, 1)
+ XCTAssertEqual(countlyMock.beginSession_Invocations.count, 1)
}
+ @MainActor
func testUpdateCurrentUser_no_current_user() async throws {
// Given tracking is enabled.
try await sut.enableTracking()
@@ -221,10 +225,11 @@ class AnalyticsServiceTests: XCTestCase {
try sut.updateCurrentUser(Scaffolding.user)
// Then the user was not updated.
- XCTAssertEqual(countly.changeDeviceIDMergeData_Invocations.count, 0)
- XCTAssertEqual(countly.setUserValueForKey_Invocations.count, 0)
+ XCTAssertEqual(countlyMock.changeDeviceIDMergeData_Invocations.count, 0)
+ XCTAssertEqual(countlyMock.setUserValueForKey_Invocations.count, 0)
}
+ @MainActor
func testUpdateCurrentUser_no_change() async throws {
// Given tracking is enabled.
try await sut.enableTracking()
@@ -237,10 +242,11 @@ class AnalyticsServiceTests: XCTestCase {
try sut.updateCurrentUser(Scaffolding.user)
// Then no user data changed.
- XCTAssertEqual(countly.changeDeviceIDMergeData_Invocations.count, 0)
- XCTAssertEqual(countly.setUserValueForKey_Invocations.count, 0)
+ XCTAssertEqual(countlyMock.changeDeviceIDMergeData_Invocations.count, 0)
+ XCTAssertEqual(countlyMock.setUserValueForKey_Invocations.count, 0)
}
+ @MainActor
func testUpdateCurrentUser_with_change() async throws {
// Given tracking is enabled.
try await sut.enableTracking()
@@ -253,7 +259,7 @@ class AnalyticsServiceTests: XCTestCase {
try sut.updateCurrentUser(Scaffolding.userWithTeam)
// Then the device id was changed with a merge.
- let deviceChangeInvocations = countly.changeDeviceIDMergeData_Invocations
+ let deviceChangeInvocations = countlyMock.changeDeviceIDMergeData_Invocations
guard deviceChangeInvocations.count == 1 else {
XCTFail("expected 1 device change invocation, got \(deviceChangeInvocations.count)")
return
@@ -263,7 +269,7 @@ class AnalyticsServiceTests: XCTestCase {
XCTAssertEqual(deviceChangeInvocations[0].mergeData, true)
// Then the user details were set.
- let userSetInvocations = countly.setUserValueForKey_Invocations
+ let userSetInvocations = countlyMock.setUserValueForKey_Invocations
guard userSetInvocations.count == 3 else {
XCTFail("expected 3 user set invocations, got \(userSetInvocations.count)")
return
@@ -285,9 +291,10 @@ class AnalyticsServiceTests: XCTestCase {
sut.trackEvent(Scaffolding.event)
// Then no event was tracked.
- XCTAssertEqual(countly.recordEventSegmentation_Invocations.count, 0)
+ XCTAssertEqual(countlyMock.recordEventSegmentation_Invocations.count, 0)
}
+ @MainActor
func testTrackEvent_no_current_user() async throws {
// Given tracking is enabled.
try await sut.enableTracking()
@@ -298,9 +305,10 @@ class AnalyticsServiceTests: XCTestCase {
sut.trackEvent(Scaffolding.event)
// Then no event was tracked.
- XCTAssertEqual(countly.recordEventSegmentation_Invocations.count, 0)
+ XCTAssertEqual(countlyMock.recordEventSegmentation_Invocations.count, 0)
}
+ @MainActor
func testTrackEvent_succeeds() async throws {
// Given tracking is enabled.
try await sut.enableTracking()
@@ -312,7 +320,7 @@ class AnalyticsServiceTests: XCTestCase {
sut.trackEvent(Scaffolding.event)
// Then a single event was tracked.
- let recordInvocations = countly.recordEventSegmentation_Invocations
+ let recordInvocations = countlyMock.recordEventSegmentation_Invocations
guard recordInvocations.count == 1 else {
XCTFail("expected 1 recordInvocation, got \(recordInvocations.count)")
return
diff --git a/WireDomain/.swiftpm/xcode/xcshareddata/xcschemes/WireDomainPackage.xcscheme b/WireDomain/.swiftpm/xcode/xcshareddata/xcschemes/WireDomainPackage.xcscheme
deleted file mode 100644
index 1ee80a9c74b..00000000000
--- a/WireDomain/.swiftpm/xcode/xcshareddata/xcschemes/WireDomainPackage.xcscheme
+++ /dev/null
@@ -1,84 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/WireDomain/.swiftpm/xcode/xcshareddata/xcschemes/WireDomainPackageSupport.xcscheme b/WireDomain/.swiftpm/xcode/xcshareddata/xcschemes/WireDomainPackageSupport.xcscheme
deleted file mode 100644
index daa4f774309..00000000000
--- a/WireDomain/.swiftpm/xcode/xcshareddata/xcschemes/WireDomainPackageSupport.xcscheme
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/WireDomain/Package.resolved b/WireDomain/Package.resolved
new file mode 100644
index 00000000000..3b2eb2263f3
--- /dev/null
+++ b/WireDomain/Package.resolved
@@ -0,0 +1,42 @@
+{
+ "originHash" : "b0b8539ea418da22b07d0a75a40c21caa3f909f7b2c177680c5fdc645fc1d64a",
+ "pins" : [
+ {
+ "identity" : "swift-docc-plugin",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swiftlang/swift-docc-plugin",
+ "state" : {
+ "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64",
+ "version" : "1.4.3"
+ }
+ },
+ {
+ "identity" : "swift-docc-symbolkit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swiftlang/swift-docc-symbolkit",
+ "state" : {
+ "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
+ "version" : "1.0.0"
+ }
+ },
+ {
+ "identity" : "swift-snapshot-testing",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
+ "state" : {
+ "revision" : "42a086182681cf661f5c47c9b7dc3931de18c6d7",
+ "version" : "1.17.6"
+ }
+ },
+ {
+ "identity" : "swift-syntax",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swiftlang/swift-syntax",
+ "state" : {
+ "revision" : "0687f71944021d616d34d922343dcef086855920",
+ "version" : "600.0.1"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/WireDomain/Package.swift b/WireDomain/Package.swift
index a294d3871df..e4dee31641b 100644
--- a/WireDomain/Package.swift
+++ b/WireDomain/Package.swift
@@ -7,6 +7,7 @@ let package = Package(
name: "WireDomainPackage",
platforms: [.iOS(.v16), .macOS(.v12)],
products: [
+ .library(name: "WireDomainAPI", targets: ["WireDomainAPI"]),
.library(name: "WireDomainPackage", targets: ["WireDomainPkg"]),
.library(name: "WireDomainPackageSupport", targets: ["WireDomainPkgSupport"])
],
@@ -14,12 +15,17 @@ let package = Package(
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
.package(path: "../WirePlugins"),
.package(name: "WireAPI", path: "../WireAPI"),
+ .package(name: "WireAnalytics", path: "../WireAnalytics"),
.package(name: "WireFoundation", path: "../WireFoundation")
],
targets: [
+ .target(
+ name: "WireDomainAPI",
+ path: "./Sources/WireDomainAPI"
+ ),
.target(
name: "WireDomainPkg",
- dependencies: ["WireAPI"],
+ dependencies: ["WireAPI", "WireAnalytics"],
path: "./Sources/Package"
),
.target(
diff --git a/WireDomain/Project/WireDomain.xctestplan b/WireDomain/Project/TestPlans/AllTests.xctestplan
similarity index 67%
rename from WireDomain/Project/WireDomain.xctestplan
rename to WireDomain/Project/TestPlans/AllTests.xctestplan
index 7d9e6118a78..8607d870983 100644
--- a/WireDomain/Project/WireDomain.xctestplan
+++ b/WireDomain/Project/TestPlans/AllTests.xctestplan
@@ -1,7 +1,7 @@
{
"configurations" : [
{
- "id" : "7460DDE4-418D-4C73-B293-D9F903D9461E",
+ "id" : "9D1D0475-9029-44FF-9FE3-BECDF3815511",
"name" : "Configuration 1",
"options" : {
@@ -9,11 +9,9 @@
}
],
"defaultOptions" : {
- "commandLineArgumentEntries" : [
- {
- "argument" : "-com.apple.CoreData.ConcurrencyDebug 1"
- }
- ]
+ "language" : "en",
+ "region" : "DE",
+ "testExecutionOrdering" : "random"
},
"testTargets" : [
{
diff --git a/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj b/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj
index c35b277f3a2..b223a391cbc 100644
--- a/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj
+++ b/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj
@@ -3,144 +3,26 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 56;
+ objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
0163EE812C20D71C00B37260 /* WireDomain.docc in Sources */ = {isa = PBXBuildFile; fileRef = 01D0DCE92C1C8EA10076CB1C /* WireDomain.docc */; };
017F67942C207AA000B6E02D /* WireDomainSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 017F67822C207A3200B6E02D /* WireDomainSupport.framework */; };
- 017F679C2C20801800B6E02D /* TeamRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017F67982C20801800B6E02D /* TeamRepositoryTests.swift */; };
- 017F67AF2C20803300B6E02D /* AutoMockable.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017F67A42C20802300B6E02D /* AutoMockable.generated.swift */; };
01D0DCB12C1C8C880076CB1C /* WireDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01D0DCA62C1C8C870076CB1C /* WireDomain.framework */; };
01D0DCC62C1C8CD90076CB1C /* WireTransport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01D0DCC52C1C8CD90076CB1C /* WireTransport.framework */; };
- 162356502C2B223100C6666C /* UserRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1623564F2C2B223100C6666C /* UserRepositoryTests.swift */; };
591B6E452C8B09BA009F8A7B /* WireDataModel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01D0DCC32C1C8CC20076CB1C /* WireDataModel.framework */; };
591B6E472C8B09BD009F8A7B /* WireDataModelSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01BDA5442C20762200636E50 /* WireDataModelSupport.framework */; };
+ 594904952D0710BF00238104 /* WireAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 594904942D0710BF00238104 /* WireAnalytics */; };
598D042D2C89C63100B64D71 /* WireFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 598D042C2C89C63100B64D71 /* WireFoundation */; };
59909A5E2C5BBEA8009C41DE /* WireAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 59909A5D2C5BBEA8009C41DE /* WireAPI */; };
59909A692C5BC001009C41DE /* WireAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 59909A682C5BC001009C41DE /* WireAPI */; };
59909A6D2C5BC08B009C41DE /* WireAPISupport in Frameworks */ = {isa = PBXBuildFile; productRef = 59909A6C2C5BC08B009C41DE /* WireAPISupport */; };
- C91F111C2C9C4B2B00BA5BE2 /* ConnectionsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91F111A2C9C4B2B00BA5BE2 /* ConnectionsRepository.swift */; };
- C91F111D2C9C4B2B00BA5BE2 /* ConnectionsLocalStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91F111B2C9C4B2B00BA5BE2 /* ConnectionsLocalStore.swift */; };
- C93961922C91B12800EA971A /* TestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0E117D2C2C4080004BBD29 /* TestError.swift */; };
- C93961932C91B15B00EA971A /* ConversationRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E8A3E72C7F6EA40093DD5C /* ConversationRepositoryTests.swift */; };
- C96B75292CDB7688003A85EB /* ConversationMemberUpdateEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96B75252CDB7688003A85EB /* ConversationMemberUpdateEventProcessorTests.swift */; };
- C96B752A2CDB7688003A85EB /* ConversationMemberJoinEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96B75232CDB7688003A85EB /* ConversationMemberJoinEventTests.swift */; };
- C96B752B2CDB7688003A85EB /* ConversationProtocolUpdateEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96B75262CDB7688003A85EB /* ConversationProtocolUpdateEventProcessorTests.swift */; };
- C96B752C2CDB7688003A85EB /* ConversationAccessUpdateEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96B75212CDB7688003A85EB /* ConversationAccessUpdateEventProcessorTests.swift */; };
- C96B752D2CDB7688003A85EB /* ConversationCreateEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96B75222CDB7688003A85EB /* ConversationCreateEventProcessorTests.swift */; };
- C96B752E2CDB7688003A85EB /* ConversationReceiptModeUpdateEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96B75272CDB7688003A85EB /* ConversationReceiptModeUpdateEventProcessorTests.swift */; };
- C96B752F2CDB7688003A85EB /* ConversationMemberLeaveEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96B75242CDB7688003A85EB /* ConversationMemberLeaveEventTests.swift */; };
- C96B75432CDB8E03003A85EB /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96B75422CDB8E03003A85EB /* NSManagedObjectContext.swift */; };
- C96B75452CDBA0A9003A85EB /* ConversationDeleteEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96B75442CDBA0A9003A85EB /* ConversationDeleteEventProcessorTests.swift */; };
- C96B75482CDBA10F003A85EB /* SystemMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96B75462CDBA10F003A85EB /* SystemMessage.swift */; };
+ 59EA774F2D00CE0C002CA0B8 /* WireLogging in Frameworks */ = {isa = PBXBuildFile; productRef = 59EA774E2D00CE0C002CA0B8 /* WireLogging */; };
+ 766BE3C52D036D5000148CB6 /* WireDomainAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 766BE3C42D036D5000148CB6 /* WireDomainAPI */; };
C97BCCAA2C98704B004F2D0D /* WireDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01D0DCA62C1C8C870076CB1C /* WireDomain.framework */; };
- C97C013D2CAD7D69000683C5 /* UserPropertiesDeleteEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C013C2CAD7D69000683C5 /* UserPropertiesDeleteEventProcessorTests.swift */; };
- C97C014B2CB00F92000683C5 /* OneOnOneResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C014A2CB00F92000683C5 /* OneOnOneResolver.swift */; };
- C97C01502CB01BDF000683C5 /* OneOnOneResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C014F2CB01BDF000683C5 /* OneOnOneResolverTests.swift */; };
- C97C01522CB01BEF000683C5 /* UserConnectionEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01512CB01BEF000683C5 /* UserConnectionEventProcessorTests.swift */; };
- C97C01542CB04626000683C5 /* UserDeleteEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01532CB04626000683C5 /* UserDeleteEventProcessorTests.swift */; };
- C97C01592CB40010000683C5 /* FederationConnectionRemovedEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01562CB40010000683C5 /* FederationConnectionRemovedEventProcessorTests.swift */; };
- C97C015A2CB40010000683C5 /* FederationDeleteEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01572CB40010000683C5 /* FederationDeleteEventProcessorTests.swift */; };
- C97C015C2CB40038000683C5 /* UserPropertiesSetEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C015B2CB40038000683C5 /* UserPropertiesSetEventProcessorTests.swift */; };
- C97C015F2CB40EF3000683C5 /* PushSupportedProtocolsUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C015E2CB40EF3000683C5 /* PushSupportedProtocolsUseCaseTests.swift */; };
- C97C01A52CB80F54000683C5 /* UserClientsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01A42CB80F4E000683C5 /* UserClientsRepository.swift */; };
- C97C01A82CB813C9000683C5 /* UserClientsRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01A72CB813C9000683C5 /* UserClientsRepositoryTests.swift */; };
- C97C01AC2CB92D47000683C5 /* UserLegalholdEnableEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01AB2CB92D47000683C5 /* UserLegalholdEnableEventProcessorTests.swift */; };
- C97C01BB2CBE5E65000683C5 /* UserUpdateEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01BA2CBE5E65000683C5 /* UserUpdateEventProcessorTests.swift */; };
- C97C01D72CC13E1A000683C5 /* UserLocalStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01D62CC13E1A000683C5 /* UserLocalStore.swift */; };
- C9841A1F2CA309310062A834 /* MockFeatureConfigRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9841A1E2CA309310062A834 /* MockFeatureConfigRepositoryProtocol.swift */; };
- C98433E72CCA868F009723D4 /* MessageRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98433E62CCA868F009723D4 /* MessageRepository.swift */; };
- C98433E92CCA86CA009723D4 /* MessageLocalStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98433E82CCA86CA009723D4 /* MessageLocalStore.swift */; };
- C98433EF2CCB7EE6009723D4 /* MessageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98433EE2CCB7EE6009723D4 /* MessageType.swift */; };
- C98433F12CCBC251009723D4 /* MessageRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98433F02CCBC251009723D4 /* MessageRepositoryTests.swift */; };
- C98433F52CCBC290009723D4 /* MessageLocalStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98433F42CCBC290009723D4 /* MessageLocalStoreTests.swift */; };
- C98433D02CC26A1D009723D4 /* MLSProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98433CF2CC26A1D009723D4 /* MLSProvider.swift */; };
- C99322D22C986E3A0065E10F /* TeamRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B22C986E3A0065E10F /* TeamRepository.swift */; };
- C99322D32C986E3A0065E10F /* TeamRepositoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B32C986E3A0065E10F /* TeamRepositoryError.swift */; };
- C99322D42C986E3A0065E10F /* SelfUserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B52C986E3A0065E10F /* SelfUserProvider.swift */; };
- C99322D52C986E3A0065E10F /* UserModelMappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B62C986E3A0065E10F /* UserModelMappings.swift */; };
- C99322D62C986E3A0065E10F /* UserRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B72C986E3A0065E10F /* UserRepository.swift */; };
- C99322D72C986E3A0065E10F /* UserRepositoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B82C986E3A0065E10F /* UserRepositoryError.swift */; };
- C99322D82C986E3A0065E10F /* UpdateEventsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322BA2C986E3A0065E10F /* UpdateEventsRepository.swift */; };
- C99322D92C986E3A0065E10F /* UpdateEventsRepositoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322BB2C986E3A0065E10F /* UpdateEventsRepositoryError.swift */; };
- C99322DB2C986E3A0065E10F /* FeatureConfigModelMappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322BF2C986E3A0065E10F /* FeatureConfigModelMappings.swift */; };
- C99322DC2C986E3A0065E10F /* FeatureConfigRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322C02C986E3A0065E10F /* FeatureConfigRepository.swift */; };
- C99322DD2C986E3A0065E10F /* FeatureConfigRepositoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322C12C986E3A0065E10F /* FeatureConfigRepositoryError.swift */; };
- C99322DE2C986E3A0065E10F /* ConversationLocalStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322C32C986E3A0065E10F /* ConversationLocalStore.swift */; };
- C99322DF2C986E3A0065E10F /* ConversationLocalStore+Group.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322C42C986E3A0065E10F /* ConversationLocalStore+Group.swift */; };
- C99322E02C986E3A0065E10F /* ConversationLocalStore+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322C52C986E3A0065E10F /* ConversationLocalStore+Metadata.swift */; };
- C99322E12C986E3A0065E10F /* ConversationLocalStore+MLS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322C62C986E3A0065E10F /* ConversationLocalStore+MLS.swift */; };
- C99322E22C986E3A0065E10F /* ConversationLocalStore+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322C72C986E3A0065E10F /* ConversationLocalStore+Status.swift */; };
- C99322E32C986E3A0065E10F /* ConversationModelMappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322C82C986E3A0065E10F /* ConversationModelMappings.swift */; };
- C99322E42C986E3A0065E10F /* ConversationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322C92C986E3A0065E10F /* ConversationRepository.swift */; };
- C99322E52C986E3A0065E10F /* ConversationRepositoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322CA2C986E3A0065E10F /* ConversationRepositoryError.swift */; };
- C99322E72C986E3A0065E10F /* ConnectionsRepositoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322CD2C986E3A0065E10F /* ConnectionsRepositoryError.swift */; };
- C99322E82C986E3A0065E10F /* ConversationLabelsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322CF2C986E3A0065E10F /* ConversationLabelsRepository.swift */; };
- C99322E92C986E3A0065E10F /* ConversationLabelsRepositoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322D02C986E3A0065E10F /* ConversationLabelsRepositoryError.swift */; };
- C9C8FDCE2C9DBE0E00702B91 /* FeatureConfigUpdateEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C8FDC32C9DBE0E00702B91 /* FeatureConfigUpdateEventProcessorTests.swift */; };
- C9C8FDCF2C9DBE0E00702B91 /* TeamDeleteEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C8FDC52C9DBE0E00702B91 /* TeamDeleteEventProcessorTests.swift */; };
- C9C8FDD02C9DBE0E00702B91 /* TeamMemberLeaveEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C8FDC62C9DBE0E00702B91 /* TeamMemberLeaveEventProcessorTests.swift */; };
- C9C8FDD12C9DBE0E00702B91 /* TeamMemberUpdateEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C8FDC72C9DBE0E00702B91 /* TeamMemberUpdateEventProcessorTests.swift */; };
- C9C8FDD22C9DBE0E00702B91 /* UserClientAddEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C8FDC92C9DBE0E00702B91 /* UserClientAddEventProcessorTests.swift */; };
- C9C8FDD32C9DBE0E00702B91 /* UserLegalHoldDisableEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C8FDCA2C9DBE0E00702B91 /* UserLegalHoldDisableEventProcessorTests.swift */; };
- C9C8FDD42C9DBE0E00702B91 /* UserLegalholdRequestEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C8FDCB2C9DBE0E00702B91 /* UserLegalholdRequestEventProcessorTests.swift */; };
C9E0C9BB2C91B76F00CE6607 /* WireTestingPackage in Frameworks */ = {isa = PBXBuildFile; productRef = C9E0C9BA2C91B76F00CE6607 /* WireTestingPackage */; };
- C9E8A3AE2C73878B0093DD5C /* ConnectionsRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E8A3AD2C73878B0093DD5C /* ConnectionsRepositoryTests.swift */; };
- C9E8A3B72C749F2A0093DD5C /* ConversationLabelsRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E8A3B62C749F2A0093DD5C /* ConversationLabelsRepositoryTests.swift */; };
- C9E8A3C02C761EDD0093DD5C /* FeatureConfigRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E8A3BF2C761EDD0093DD5C /* FeatureConfigRepositoryTests.swift */; };
- C9EA769F2C92DD0F00A7D35C /* PushSupportedProtocolsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9EA769E2C92DD0F00A7D35C /* PushSupportedProtocolsUseCase.swift */; };
- C9F691292C9B164A008CC41F /* UserPushRemoveEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F691282C9B164A008CC41F /* UserPushRemoveEventProcessorTests.swift */; };
- C9FDF3EC2CAA988900D78098 /* ConnectionsModelMappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FDF3EA2CAA988100D78098 /* ConnectionsModelMappings.swift */; };
CB7979132C738508006FBA58 /* WireTransportSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB7979122C738508006FBA58 /* WireTransportSupport.framework */; };
- CB7979162C738547006FBA58 /* TestSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB7979152C738547006FBA58 /* TestSetup.swift */; };
- EE368CCE2C2DAA87009DBAB0 /* ConversationEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE368CC72C2DAA87009DBAB0 /* ConversationEventProcessor.swift */; };
- EE368CCF2C2DAA87009DBAB0 /* FeatureConfigEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE368CC82C2DAA87009DBAB0 /* FeatureConfigEventProcessor.swift */; };
- EE368CD02C2DAA87009DBAB0 /* FederationEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE368CC92C2DAA87009DBAB0 /* FederationEventProcessor.swift */; };
- EE368CD12C2DAA87009DBAB0 /* TeamEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE368CCA2C2DAA87009DBAB0 /* TeamEventProcessor.swift */; };
- EE368CD22C2DAA87009DBAB0 /* UpdateEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE368CCB2C2DAA87009DBAB0 /* UpdateEventProcessor.swift */; };
- EE368CD32C2DAA87009DBAB0 /* UserEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE368CCC2C2DAA87009DBAB0 /* UserEventProcessor.swift */; };
- EE3F97542C2ADC4C00668DF1 /* ProteusMessageDecryptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3F97532C2ADC4C00668DF1 /* ProteusMessageDecryptorTests.swift */; };
- EE57A6FE2C298F380096F242 /* UpdateEventDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE57A6FD2C298F380096F242 /* UpdateEventDecryptor.swift */; };
- EE57A7002C298F630096F242 /* ProteusMessageDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE57A6FF2C298F630096F242 /* ProteusMessageDecryptor.swift */; };
- EE57A7032C2994420096F242 /* UpdateEventsRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE57A7022C2994420096F242 /* UpdateEventsRepositoryTests.swift */; };
- EE57A7082C2A8B740096F242 /* ProteusMessageDecryptorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE57A7062C2A86880096F242 /* ProteusMessageDecryptorError.swift */; };
- EE57A70B2C2A8BAA0096F242 /* UpdateEventDecryptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE57A70A2C2A8BAA0096F242 /* UpdateEventDecryptorTests.swift */; };
- EEAD09F22C46604400CC8658 /* ConversationAccessUpdateEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD09F12C46604400CC8658 /* ConversationAccessUpdateEventProcessor.swift */; };
- EEAD09F42C46709800CC8658 /* ConversationCodeUpdateEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD09F32C46709800CC8658 /* ConversationCodeUpdateEventProcessor.swift */; };
- EEAD09F62C46772000CC8658 /* ConversationCreateEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD09F52C46772000CC8658 /* ConversationCreateEventProcessor.swift */; };
- EEAD09F82C46772C00CC8658 /* ConversationDeleteEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD09F72C46772C00CC8658 /* ConversationDeleteEventProcessor.swift */; };
- EEAD09FA2C46773300CC8658 /* ConversationMemberJoinEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD09F92C46773300CC8658 /* ConversationMemberJoinEventProcessor.swift */; };
- EEAD09FC2C46773900CC8658 /* ConversationMemberLeaveEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD09FB2C46773900CC8658 /* ConversationMemberLeaveEventProcessor.swift */; };
- EEAD09FE2C46774200CC8658 /* ConversationMemberUpdateEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD09FD2C46774200CC8658 /* ConversationMemberUpdateEventProcessor.swift */; };
- EEAD0A002C46774900CC8658 /* ConversationMessageTimerUpdateEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD09FF2C46774900CC8658 /* ConversationMessageTimerUpdateEventProcessor.swift */; };
- EEAD0A022C46774F00CC8658 /* ConversationMLSMessageAddEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A012C46774F00CC8658 /* ConversationMLSMessageAddEventProcessor.swift */; };
- EEAD0A042C46775700CC8658 /* ConversationMLSWelcomeEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A032C46775700CC8658 /* ConversationMLSWelcomeEventProcessor.swift */; };
- EEAD0A062C46775D00CC8658 /* ConversationProteusMessageAddEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A052C46775D00CC8658 /* ConversationProteusMessageAddEventProcessor.swift */; };
- EEAD0A082C46776400CC8658 /* ConversationProtocolUpdateEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A072C46776400CC8658 /* ConversationProtocolUpdateEventProcessor.swift */; };
- EEAD0A0A2C46776B00CC8658 /* ConversationReceiptModeUpdateEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A092C46776B00CC8658 /* ConversationReceiptModeUpdateEventProcessor.swift */; };
- EEAD0A0C2C46777200CC8658 /* ConversationRenameEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A0B2C46777200CC8658 /* ConversationRenameEventProcessor.swift */; };
- EEAD0A0E2C46777800CC8658 /* ConversationTypingEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A0D2C46777800CC8658 /* ConversationTypingEventProcessor.swift */; };
- EEAD0A112C46A33E00CC8658 /* TeamMemberUpdateEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A102C46A33E00CC8658 /* TeamMemberUpdateEventProcessor.swift */; };
- EEAD0A132C46A38200CC8658 /* TeamMemberLeaveEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A122C46A38200CC8658 /* TeamMemberLeaveEventProcessor.swift */; };
- EEAD0A152C46A3AB00CC8658 /* TeamDeleteEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A142C46A3AB00CC8658 /* TeamDeleteEventProcessor.swift */; };
- EEAD0A182C46A88D00CC8658 /* UserClientAddEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A172C46A88D00CC8658 /* UserClientAddEventProcessor.swift */; };
- EEAD0A1A2C46A92000CC8658 /* UserClientRemoveEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A192C46A92000CC8658 /* UserClientRemoveEventProcessor.swift */; };
- EEAD0A1C2C46A99D00CC8658 /* UserConnectionEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A1B2C46A99D00CC8658 /* UserConnectionEventProcessor.swift */; };
- EEAD0A202C46AB8700CC8658 /* UserDeleteEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A1F2C46AB8700CC8658 /* UserDeleteEventProcessor.swift */; };
- EEAD0A222C46ABFB00CC8658 /* UserLegalholdDisableEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A212C46ABFA00CC8658 /* UserLegalholdDisableEventProcessor.swift */; };
- EEAD0A242C46ACEA00CC8658 /* UserLegalholdEnableEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A232C46ACEA00CC8658 /* UserLegalholdEnableEventProcessor.swift */; };
- EEAD0A262C46AD4D00CC8658 /* UserLegalholdRequestEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A252C46AD4D00CC8658 /* UserLegalholdRequestEventProcessor.swift */; };
- EEAD0A282C46AE6400CC8658 /* UserPropertiesSetEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A272C46AE6400CC8658 /* UserPropertiesSetEventProcessor.swift */; };
- EEAD0A2A2C46AEB600CC8658 /* UserPropertiesDeleteEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A292C46AEB600CC8658 /* UserPropertiesDeleteEventProcessor.swift */; };
- EEAD0A2C2C46AF9200CC8658 /* UserPushRemoveEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A2B2C46AF9200CC8658 /* UserPushRemoveEventProcessor.swift */; };
- EEAD0A2E2C46B01900CC8658 /* UserUpdateEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A2D2C46B01900CC8658 /* UserUpdateEventProcessor.swift */; };
- EEAD0A312C46B98D00CC8658 /* FederationConnectionRemovedEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A302C46B98D00CC8658 /* FederationConnectionRemovedEventProcessor.swift */; };
- EEAD0A332C46B99800CC8658 /* FederationDeleteEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A322C46B99800CC8658 /* FederationDeleteEventProcessor.swift */; };
- EEAD0A362C46BBA600CC8658 /* FeatureConfigUpdateEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD0A352C46BBA600CC8658 /* FeatureConfigUpdateEventProcessor.swift */; };
- EEC410262C60D48900E89394 /* SyncManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC410242C60D48900E89394 /* SyncManagerTests.swift */; };
- EECC35A62C2EB6CD00679448 /* SyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECC35A52C2EB6CD00679448 /* SyncManager.swift */; };
- EECC35A82C2EB70400679448 /* SyncState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECC35A72C2EB70400679448 /* SyncState.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -162,139 +44,32 @@
/* Begin PBXFileReference section */
017F67822C207A3200B6E02D /* WireDomainSupport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WireDomainSupport.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- 017F67982C20801800B6E02D /* TeamRepositoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamRepositoryTests.swift; sourceTree = ""; };
- 017F67A42C20802300B6E02D /* AutoMockable.generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoMockable.generated.swift; sourceTree = ""; };
- 017F67A62C20802300B6E02D /* AutoMockable.stencil */ = {isa = PBXFileReference; lastKnownFileType = text; path = AutoMockable.stencil; sourceTree = ""; };
- 017F67A72C20802300B6E02D /* config.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = config.yml; sourceTree = ""; };
01BDA5442C20762200636E50 /* WireDataModelSupport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WireDataModelSupport.framework; sourceTree = BUILT_PRODUCTS_DIR; };
01D0DCA62C1C8C870076CB1C /* WireDomain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WireDomain.framework; sourceTree = BUILT_PRODUCTS_DIR; };
01D0DCB02C1C8C880076CB1C /* WireDomainTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WireDomainTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
01D0DCC32C1C8CC20076CB1C /* WireDataModel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WireDataModel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
01D0DCC52C1C8CD90076CB1C /* WireTransport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WireTransport.framework; sourceTree = BUILT_PRODUCTS_DIR; };
01D0DCE92C1C8EA10076CB1C /* WireDomain.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = WireDomain.docc; sourceTree = ""; };
- 01D0DCFC2C1C8F9B0076CB1C /* WireDomain.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = WireDomain.xctestplan; sourceTree = ""; };
- 1623564F2C2B223100C6666C /* UserRepositoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserRepositoryTests.swift; sourceTree = ""; };
- C91F111A2C9C4B2B00BA5BE2 /* ConnectionsRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionsRepository.swift; sourceTree = ""; };
- C91F111B2C9C4B2B00BA5BE2 /* ConnectionsLocalStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionsLocalStore.swift; sourceTree = ""; };
- C96B75212CDB7688003A85EB /* ConversationAccessUpdateEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationAccessUpdateEventProcessorTests.swift; sourceTree = ""; };
- C96B75222CDB7688003A85EB /* ConversationCreateEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCreateEventProcessorTests.swift; sourceTree = ""; };
- C96B75232CDB7688003A85EB /* ConversationMemberJoinEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMemberJoinEventTests.swift; sourceTree = ""; };
- C96B75242CDB7688003A85EB /* ConversationMemberLeaveEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMemberLeaveEventTests.swift; sourceTree = ""; };
- C96B75252CDB7688003A85EB /* ConversationMemberUpdateEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMemberUpdateEventProcessorTests.swift; sourceTree = ""; };
- C96B75262CDB7688003A85EB /* ConversationProtocolUpdateEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationProtocolUpdateEventProcessorTests.swift; sourceTree = ""; };
- C96B75272CDB7688003A85EB /* ConversationReceiptModeUpdateEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationReceiptModeUpdateEventProcessorTests.swift; sourceTree = ""; };
- C96B75422CDB8E03003A85EB /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = ""; };
- C96B75442CDBA0A9003A85EB /* ConversationDeleteEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationDeleteEventProcessorTests.swift; sourceTree = ""; };
- C96B75462CDBA10F003A85EB /* SystemMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemMessage.swift; sourceTree = ""; };
- C97C013C2CAD7D69000683C5 /* UserPropertiesDeleteEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesDeleteEventProcessorTests.swift; sourceTree = ""; };
- C97C014A2CB00F92000683C5 /* OneOnOneResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OneOnOneResolver.swift; path = ../../OneOnOneResolver.swift; sourceTree = ""; };
- C97C014F2CB01BDF000683C5 /* OneOnOneResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOnOneResolverTests.swift; sourceTree = ""; };
- C97C01512CB01BEF000683C5 /* UserConnectionEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConnectionEventProcessorTests.swift; sourceTree = ""; };
- C97C01532CB04626000683C5 /* UserDeleteEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDeleteEventProcessorTests.swift; sourceTree = ""; };
- C97C01562CB40010000683C5 /* FederationConnectionRemovedEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederationConnectionRemovedEventProcessorTests.swift; sourceTree = ""; };
- C97C01572CB40010000683C5 /* FederationDeleteEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederationDeleteEventProcessorTests.swift; sourceTree = ""; };
- C97C015B2CB40038000683C5 /* UserPropertiesSetEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesSetEventProcessorTests.swift; sourceTree = ""; };
- C97C015E2CB40EF3000683C5 /* PushSupportedProtocolsUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSupportedProtocolsUseCaseTests.swift; sourceTree = ""; };
- C97C01A42CB80F4E000683C5 /* UserClientsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserClientsRepository.swift; sourceTree = ""; };
- C97C01A72CB813C9000683C5 /* UserClientsRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserClientsRepositoryTests.swift; sourceTree = ""; };
- C97C01AB2CB92D47000683C5 /* UserLegalholdEnableEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLegalholdEnableEventProcessorTests.swift; sourceTree = ""; };
- C97C01BA2CBE5E65000683C5 /* UserUpdateEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserUpdateEventProcessorTests.swift; sourceTree = ""; };
- C97C01D62CC13E1A000683C5 /* UserLocalStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocalStore.swift; sourceTree = ""; };
- C9841A1E2CA309310062A834 /* MockFeatureConfigRepositoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFeatureConfigRepositoryProtocol.swift; sourceTree = ""; };
- C98433E62CCA868F009723D4 /* MessageRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRepository.swift; sourceTree = ""; };
- C98433E82CCA86CA009723D4 /* MessageLocalStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLocalStore.swift; sourceTree = ""; };
- C98433EE2CCB7EE6009723D4 /* MessageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageType.swift; sourceTree = ""; };
- C98433F02CCBC251009723D4 /* MessageRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRepositoryTests.swift; sourceTree = ""; };
- C98433F42CCBC290009723D4 /* MessageLocalStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLocalStoreTests.swift; sourceTree = ""; };
- C98433CF2CC26A1D009723D4 /* MLSProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLSProvider.swift; sourceTree = ""; };
- C99322B22C986E3A0065E10F /* TeamRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamRepository.swift; sourceTree = ""; };
- C99322B32C986E3A0065E10F /* TeamRepositoryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamRepositoryError.swift; sourceTree = ""; };
- C99322B52C986E3A0065E10F /* SelfUserProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelfUserProvider.swift; sourceTree = ""; };
- C99322B62C986E3A0065E10F /* UserModelMappings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserModelMappings.swift; sourceTree = ""; };
- C99322B72C986E3A0065E10F /* UserRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserRepository.swift; sourceTree = ""; };
- C99322B82C986E3A0065E10F /* UserRepositoryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserRepositoryError.swift; sourceTree = ""; };
- C99322BA2C986E3A0065E10F /* UpdateEventsRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateEventsRepository.swift; sourceTree = ""; };
- C99322BB2C986E3A0065E10F /* UpdateEventsRepositoryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateEventsRepositoryError.swift; sourceTree = ""; };
- C99322BF2C986E3A0065E10F /* FeatureConfigModelMappings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureConfigModelMappings.swift; sourceTree = ""; };
- C99322C02C986E3A0065E10F /* FeatureConfigRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureConfigRepository.swift; sourceTree = ""; };
- C99322C12C986E3A0065E10F /* FeatureConfigRepositoryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureConfigRepositoryError.swift; sourceTree = ""; };
- C99322C32C986E3A0065E10F /* ConversationLocalStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationLocalStore.swift; sourceTree = ""; };
- C99322C42C986E3A0065E10F /* ConversationLocalStore+Group.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConversationLocalStore+Group.swift"; sourceTree = ""; };
- C99322C52C986E3A0065E10F /* ConversationLocalStore+Metadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConversationLocalStore+Metadata.swift"; sourceTree = ""; };
- C99322C62C986E3A0065E10F /* ConversationLocalStore+MLS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConversationLocalStore+MLS.swift"; sourceTree = ""; };
- C99322C72C986E3A0065E10F /* ConversationLocalStore+Status.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConversationLocalStore+Status.swift"; sourceTree = ""; };
- C99322C82C986E3A0065E10F /* ConversationModelMappings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationModelMappings.swift; sourceTree = ""; };
- C99322C92C986E3A0065E10F /* ConversationRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationRepository.swift; sourceTree = ""; };
- C99322CA2C986E3A0065E10F /* ConversationRepositoryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationRepositoryError.swift; sourceTree = ""; };
- C99322CD2C986E3A0065E10F /* ConnectionsRepositoryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionsRepositoryError.swift; sourceTree = ""; };
- C99322CF2C986E3A0065E10F /* ConversationLabelsRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationLabelsRepository.swift; sourceTree = ""; };
- C99322D02C986E3A0065E10F /* ConversationLabelsRepositoryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationLabelsRepositoryError.swift; sourceTree = ""; };
- C9C8FDC32C9DBE0E00702B91 /* FeatureConfigUpdateEventProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureConfigUpdateEventProcessorTests.swift; sourceTree = ""; };
- C9C8FDC52C9DBE0E00702B91 /* TeamDeleteEventProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamDeleteEventProcessorTests.swift; sourceTree = ""; };
- C9C8FDC62C9DBE0E00702B91 /* TeamMemberLeaveEventProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamMemberLeaveEventProcessorTests.swift; sourceTree = ""; };
- C9C8FDC72C9DBE0E00702B91 /* TeamMemberUpdateEventProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamMemberUpdateEventProcessorTests.swift; sourceTree = ""; };
- C9C8FDC92C9DBE0E00702B91 /* UserClientAddEventProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserClientAddEventProcessorTests.swift; sourceTree = ""; };
- C9C8FDCA2C9DBE0E00702B91 /* UserLegalHoldDisableEventProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserLegalHoldDisableEventProcessorTests.swift; sourceTree = ""; };
- C9C8FDCB2C9DBE0E00702B91 /* UserLegalholdRequestEventProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserLegalholdRequestEventProcessorTests.swift; sourceTree = ""; };
- C9E8A3AD2C73878B0093DD5C /* ConnectionsRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionsRepositoryTests.swift; sourceTree = ""; };
- C9E8A3B62C749F2A0093DD5C /* ConversationLabelsRepositoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationLabelsRepositoryTests.swift; sourceTree = ""; };
- C9E8A3BF2C761EDD0093DD5C /* FeatureConfigRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureConfigRepositoryTests.swift; sourceTree = ""; };
- C9E8A3E72C7F6EA40093DD5C /* ConversationRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationRepositoryTests.swift; sourceTree = ""; };
- C9EA769E2C92DD0F00A7D35C /* PushSupportedProtocolsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSupportedProtocolsUseCase.swift; sourceTree = ""; };
- C9F691282C9B164A008CC41F /* UserPushRemoveEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPushRemoveEventProcessorTests.swift; sourceTree = ""; };
- C9FDF3EA2CAA988100D78098 /* ConnectionsModelMappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionsModelMappings.swift; sourceTree = ""; };
CB7979122C738508006FBA58 /* WireTransportSupport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WireTransportSupport.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- CB7979152C738547006FBA58 /* TestSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSetup.swift; sourceTree = ""; };
- EE0E117D2C2C4080004BBD29 /* TestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestError.swift; sourceTree = ""; };
- EE368CC72C2DAA87009DBAB0 /* ConversationEventProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationEventProcessor.swift; sourceTree = ""; };
- EE368CC82C2DAA87009DBAB0 /* FeatureConfigEventProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureConfigEventProcessor.swift; sourceTree = ""; };
- EE368CC92C2DAA87009DBAB0 /* FederationEventProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FederationEventProcessor.swift; sourceTree = ""; };
- EE368CCA2C2DAA87009DBAB0 /* TeamEventProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamEventProcessor.swift; sourceTree = ""; };
- EE368CCB2C2DAA87009DBAB0 /* UpdateEventProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateEventProcessor.swift; sourceTree = ""; };
- EE368CCC2C2DAA87009DBAB0 /* UserEventProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserEventProcessor.swift; sourceTree = ""; };
- EE3F97532C2ADC4C00668DF1 /* ProteusMessageDecryptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProteusMessageDecryptorTests.swift; sourceTree = ""; };
- EE57A6FD2C298F380096F242 /* UpdateEventDecryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateEventDecryptor.swift; sourceTree = ""; };
- EE57A6FF2C298F630096F242 /* ProteusMessageDecryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProteusMessageDecryptor.swift; sourceTree = ""; };
- EE57A7022C2994420096F242 /* UpdateEventsRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateEventsRepositoryTests.swift; sourceTree = ""; };
- EE57A7062C2A86880096F242 /* ProteusMessageDecryptorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProteusMessageDecryptorError.swift; sourceTree = ""; };
- EE57A70A2C2A8BAA0096F242 /* UpdateEventDecryptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateEventDecryptorTests.swift; sourceTree = ""; };
- EEAD09F12C46604400CC8658 /* ConversationAccessUpdateEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationAccessUpdateEventProcessor.swift; sourceTree = ""; };
- EEAD09F32C46709800CC8658 /* ConversationCodeUpdateEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCodeUpdateEventProcessor.swift; sourceTree = ""; };
- EEAD09F52C46772000CC8658 /* ConversationCreateEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCreateEventProcessor.swift; sourceTree = ""; };
- EEAD09F72C46772C00CC8658 /* ConversationDeleteEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationDeleteEventProcessor.swift; sourceTree = ""; };
- EEAD09F92C46773300CC8658 /* ConversationMemberJoinEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMemberJoinEventProcessor.swift; sourceTree = ""; };
- EEAD09FB2C46773900CC8658 /* ConversationMemberLeaveEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMemberLeaveEventProcessor.swift; sourceTree = ""; };
- EEAD09FD2C46774200CC8658 /* ConversationMemberUpdateEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMemberUpdateEventProcessor.swift; sourceTree = ""; };
- EEAD09FF2C46774900CC8658 /* ConversationMessageTimerUpdateEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMessageTimerUpdateEventProcessor.swift; sourceTree = ""; };
- EEAD0A012C46774F00CC8658 /* ConversationMLSMessageAddEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMLSMessageAddEventProcessor.swift; sourceTree = ""; };
- EEAD0A032C46775700CC8658 /* ConversationMLSWelcomeEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMLSWelcomeEventProcessor.swift; sourceTree = ""; };
- EEAD0A052C46775D00CC8658 /* ConversationProteusMessageAddEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationProteusMessageAddEventProcessor.swift; sourceTree = ""; };
- EEAD0A072C46776400CC8658 /* ConversationProtocolUpdateEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationProtocolUpdateEventProcessor.swift; sourceTree = ""; };
- EEAD0A092C46776B00CC8658 /* ConversationReceiptModeUpdateEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationReceiptModeUpdateEventProcessor.swift; sourceTree = ""; };
- EEAD0A0B2C46777200CC8658 /* ConversationRenameEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationRenameEventProcessor.swift; sourceTree = ""; };
- EEAD0A0D2C46777800CC8658 /* ConversationTypingEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTypingEventProcessor.swift; sourceTree = ""; };
- EEAD0A102C46A33E00CC8658 /* TeamMemberUpdateEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamMemberUpdateEventProcessor.swift; sourceTree = ""; };
- EEAD0A122C46A38200CC8658 /* TeamMemberLeaveEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamMemberLeaveEventProcessor.swift; sourceTree = ""; };
- EEAD0A142C46A3AB00CC8658 /* TeamDeleteEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamDeleteEventProcessor.swift; sourceTree = ""; };
- EEAD0A172C46A88D00CC8658 /* UserClientAddEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserClientAddEventProcessor.swift; sourceTree = ""; };
- EEAD0A192C46A92000CC8658 /* UserClientRemoveEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserClientRemoveEventProcessor.swift; sourceTree = ""; };
- EEAD0A1B2C46A99D00CC8658 /* UserConnectionEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConnectionEventProcessor.swift; sourceTree = ""; };
- EEAD0A1F2C46AB8700CC8658 /* UserDeleteEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDeleteEventProcessor.swift; sourceTree = ""; };
- EEAD0A212C46ABFA00CC8658 /* UserLegalholdDisableEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLegalholdDisableEventProcessor.swift; sourceTree = ""; };
- EEAD0A232C46ACEA00CC8658 /* UserLegalholdEnableEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLegalholdEnableEventProcessor.swift; sourceTree = ""; };
- EEAD0A252C46AD4D00CC8658 /* UserLegalholdRequestEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLegalholdRequestEventProcessor.swift; sourceTree = ""; };
- EEAD0A272C46AE6400CC8658 /* UserPropertiesSetEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesSetEventProcessor.swift; sourceTree = ""; };
- EEAD0A292C46AEB600CC8658 /* UserPropertiesDeleteEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesDeleteEventProcessor.swift; sourceTree = ""; };
- EEAD0A2B2C46AF9200CC8658 /* UserPushRemoveEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPushRemoveEventProcessor.swift; sourceTree = ""; };
- EEAD0A2D2C46B01900CC8658 /* UserUpdateEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserUpdateEventProcessor.swift; sourceTree = ""; };
- EEAD0A302C46B98D00CC8658 /* FederationConnectionRemovedEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederationConnectionRemovedEventProcessor.swift; sourceTree = ""; };
- EEAD0A322C46B99800CC8658 /* FederationDeleteEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederationDeleteEventProcessor.swift; sourceTree = ""; };
- EEAD0A352C46BBA600CC8658 /* FeatureConfigUpdateEventProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureConfigUpdateEventProcessor.swift; sourceTree = ""; };
- EEC410242C60D48900E89394 /* SyncManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncManagerTests.swift; sourceTree = ""; };
- EECC35A52C2EB6CD00679448 /* SyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncManager.swift; sourceTree = ""; };
- EECC35A72C2EB70400679448 /* SyncState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncState.swift; sourceTree = ""; };
/* End PBXFileReference section */
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ 59EA78D62D00CF22002CA0B8 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Sourcery/generated/AutoMockable.generated.swift,
+ );
+ target = 017F67812C207A3200B6E02D /* WireDomainSupport */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ 59EA76E82D00CD88002CA0B8 /* TestPlans */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = TestPlans; sourceTree = ""; };
+ 59EA78992D00CF1C002CA0B8 /* WireDomainTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = WireDomainTests; sourceTree = ""; };
+ 59EA78D42D00CF22002CA0B8 /* WireDomainSupport */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (59EA78D62D00CF22002CA0B8 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WireDomainSupport; sourceTree = ""; };
+ 59EA7A282D00CFB2002CA0B8 /* WireDomain */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = WireDomain; sourceTree = ""; };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
/* Begin PBXFrameworksBuildPhase section */
017F677F2C207A3200B6E02D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -309,8 +84,11 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 766BE3C52D036D5000148CB6 /* WireDomainAPI in Frameworks */,
598D042D2C89C63100B64D71 /* WireFoundation in Frameworks */,
591B6E452C8B09BA009F8A7B /* WireDataModel.framework in Frameworks */,
+ 594904952D0710BF00238104 /* WireAnalytics in Frameworks */,
+ 59EA774F2D00CE0C002CA0B8 /* WireLogging in Frameworks */,
59909A5E2C5BBEA8009C41DE /* WireAPI in Frameworks */,
01D0DCC62C1C8CD90076CB1C /* WireTransport.framework in Frameworks */,
);
@@ -332,97 +110,9 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
- 017F67992C20801800B6E02D /* Repositories */ = {
- isa = PBXGroup;
- children = (
- C9841A1D2CA309090062A834 /* Mock */,
- C9E8A3B62C749F2A0093DD5C /* ConversationLabelsRepositoryTests.swift */,
- C9E8A3AD2C73878B0093DD5C /* ConnectionsRepositoryTests.swift */,
- 017F67982C20801800B6E02D /* TeamRepositoryTests.swift */,
- EE57A7022C2994420096F242 /* UpdateEventsRepositoryTests.swift */,
- C9E8A3E72C7F6EA40093DD5C /* ConversationRepositoryTests.swift */,
- C9E8A3BF2C761EDD0093DD5C /* FeatureConfigRepositoryTests.swift */,
- 1623564F2C2B223100C6666C /* UserRepositoryTests.swift */,
- C98433F02CCBC251009723D4 /* MessageRepositoryTests.swift */,
- C97C01A72CB813C9000683C5 /* UserClientsRepositoryTests.swift */,
- );
- path = Repositories;
- sourceTree = "";
- };
- 017F679A2C20801800B6E02D /* WireDomain */ = {
- isa = PBXGroup;
- children = (
- C97C015D2CB40EBE000683C5 /* UseCases */,
- C9C8FDCD2C9DBE0E00702B91 /* Event Processing */,
- EEC410252C60D48900E89394 /* Synchronization */,
- EE0E117C2C2C4076004BBD29 /* Helpers */,
- EE57A7092C2A8B950096F242 /* Event Decryption */,
- 017F67992C20801800B6E02D /* Repositories */,
- C98433F32CCBC26E009723D4 /* LocalStores */,
- );
- name = WireDomain;
- path = WireDomainTests;
- sourceTree = "";
- };
- 017F67A22C20802300B6E02D /* Repositories */ = {
- isa = PBXGroup;
- children = (
- C98433E52CCA867F009723D4 /* Message */,
- C97C01A32CB80F46000683C5 /* UserClients */,
- C99322CE2C986E3A0065E10F /* Connections */,
- C99322CB2C986E3A0065E10F /* Conversations */,
- C99322D12C986E3A0065E10F /* ConversationsLabels */,
- C99322C22C986E3A0065E10F /* FeatureConfig */,
- C99322B42C986E3A0065E10F /* Team */,
- C99322BC2C986E3A0065E10F /* UpdateEvents */,
- C99322B92C986E3A0065E10F /* User */,
- );
- path = Repositories;
- sourceTree = "";
- };
- 017F67A32C20802300B6E02D /* WireDomain */ = {
- isa = PBXGroup;
- children = (
- C97C01C02CBE9C93000683C5 /* Extensions */,
- C9EA76AD2C98548900A7D35C /* UseCases */,
- EECC35A42C2EB6C100679448 /* Synchronization */,
- EE57A6FC2C298F280096F242 /* Event Decryption */,
- EE368CCD2C2DAA87009DBAB0 /* Event Processing */,
- 017F67A22C20802300B6E02D /* Repositories */,
- );
- path = WireDomain;
- sourceTree = "";
- };
- 017F67A52C20802300B6E02D /* generated */ = {
- isa = PBXGroup;
- children = (
- 017F67A42C20802300B6E02D /* AutoMockable.generated.swift */,
- );
- path = generated;
- sourceTree = "";
- };
- 017F67A82C20802300B6E02D /* Sourcery */ = {
- isa = PBXGroup;
- children = (
- 017F67A52C20802300B6E02D /* generated */,
- 017F67A62C20802300B6E02D /* AutoMockable.stencil */,
- 017F67A72C20802300B6E02D /* config.yml */,
- );
- path = Sourcery;
- sourceTree = "";
- };
- 017F67A92C20802300B6E02D /* WireDomainSupport */ = {
- isa = PBXGroup;
- children = (
- 017F67A82C20802300B6E02D /* Sourcery */,
- );
- path = WireDomainSupport;
- sourceTree = "";
- };
01D0DC9C2C1C8C870076CB1C = {
isa = PBXGroup;
children = (
- 01D0DCFC2C1C8F9B0076CB1C /* WireDomain.xctestplan */,
01D0DCE82C1C8EA10076CB1C /* Sources */,
01D0DCED2C1C8EA10076CB1C /* Tests */,
01D0DCE92C1C8EA10076CB1C /* WireDomain.docc */,
@@ -455,8 +145,8 @@
01D0DCE82C1C8EA10076CB1C /* Sources */ = {
isa = PBXGroup;
children = (
- 017F67A32C20802300B6E02D /* WireDomain */,
- 017F67A92C20802300B6E02D /* WireDomainSupport */,
+ 59EA7A282D00CFB2002CA0B8 /* WireDomain */,
+ 59EA78D42D00CF22002CA0B8 /* WireDomainSupport */,
);
name = Sources;
path = ../Sources;
@@ -465,374 +155,13 @@
01D0DCED2C1C8EA10076CB1C /* Tests */ = {
isa = PBXGroup;
children = (
- 017F679A2C20801800B6E02D /* WireDomain */,
+ 59EA76E82D00CD88002CA0B8 /* TestPlans */,
+ 59EA78992D00CF1C002CA0B8 /* WireDomainTests */,
);
name = Tests;
path = ../Tests;
sourceTree = "";
};
- C96B75282CDB7688003A85EB /* ConversationEventProcessor */ = {
- isa = PBXGroup;
- children = (
- C96B75212CDB7688003A85EB /* ConversationAccessUpdateEventProcessorTests.swift */,
- C96B75222CDB7688003A85EB /* ConversationCreateEventProcessorTests.swift */,
- C96B75442CDBA0A9003A85EB /* ConversationDeleteEventProcessorTests.swift */,
- C96B75232CDB7688003A85EB /* ConversationMemberJoinEventTests.swift */,
- C96B75242CDB7688003A85EB /* ConversationMemberLeaveEventTests.swift */,
- C96B75252CDB7688003A85EB /* ConversationMemberUpdateEventProcessorTests.swift */,
- C96B75262CDB7688003A85EB /* ConversationProtocolUpdateEventProcessorTests.swift */,
- C96B75272CDB7688003A85EB /* ConversationReceiptModeUpdateEventProcessorTests.swift */,
- );
- path = ConversationEventProcessor;
- sourceTree = "";
- };
- C96B75472CDBA10F003A85EB /* Models */ = {
- isa = PBXGroup;
- children = (
- C96B75462CDBA10F003A85EB /* SystemMessage.swift */,
- );
- path = Models;
- sourceTree = "";
- };
- C97C01582CB40010000683C5 /* FederationEventProcessor */ = {
- isa = PBXGroup;
- children = (
- C97C01562CB40010000683C5 /* FederationConnectionRemovedEventProcessorTests.swift */,
- C97C01572CB40010000683C5 /* FederationDeleteEventProcessorTests.swift */,
- );
- path = FederationEventProcessor;
- sourceTree = "";
- };
- C97C015D2CB40EBE000683C5 /* UseCases */ = {
- isa = PBXGroup;
- children = (
- C97C015E2CB40EF3000683C5 /* PushSupportedProtocolsUseCaseTests.swift */,
- );
- path = UseCases;
- sourceTree = "";
- };
- C97C01A32CB80F46000683C5 /* UserClients */ = {
- isa = PBXGroup;
- children = (
- C97C01A42CB80F4E000683C5 /* UserClientsRepository.swift */,
- );
- path = UserClients;
- sourceTree = "";
- };
- C97C01C02CBE9C93000683C5 /* Extensions */ = {
- isa = PBXGroup;
- children = (
- C97C01C12CBE9C9B000683C5 /* CoreData */,
- );
- path = Extensions;
- sourceTree = "";
- };
- C97C01C12CBE9C9B000683C5 /* CoreData */ = {
- isa = PBXGroup;
- children = (
- C96B75422CDB8E03003A85EB /* NSManagedObjectContext.swift */,
- );
- path = CoreData;
- sourceTree = "";
- };
- C9841A1D2CA309090062A834 /* Mock */ = {
- isa = PBXGroup;
- children = (
- C9841A1E2CA309310062A834 /* MockFeatureConfigRepositoryProtocol.swift */,
- );
- path = Mock;
- sourceTree = "";
- };
- C98433E52CCA867F009723D4 /* Message */ = {
- isa = PBXGroup;
- children = (
- C98433ED2CCB7EDF009723D4 /* Models */,
- C98433E62CCA868F009723D4 /* MessageRepository.swift */,
- C98433E82CCA86CA009723D4 /* MessageLocalStore.swift */,
- );
- path = Message;
- sourceTree = "";
- };
- C98433ED2CCB7EDF009723D4 /* Models */ = {
- isa = PBXGroup;
- children = (
- C98433EE2CCB7EE6009723D4 /* MessageType.swift */,
- );
- path = Models;
- sourceTree = "";
- };
- C98433F32CCBC26E009723D4 /* LocalStores */ = {
- isa = PBXGroup;
- children = (
- C98433F42CCBC290009723D4 /* MessageLocalStoreTests.swift */,
- );
- path = LocalStores;
- sourceTree = "";
- };
- C99322B42C986E3A0065E10F /* Team */ = {
- isa = PBXGroup;
- children = (
- C99322B22C986E3A0065E10F /* TeamRepository.swift */,
- C99322B32C986E3A0065E10F /* TeamRepositoryError.swift */,
- );
- path = Team;
- sourceTree = "";
- };
- C99322B92C986E3A0065E10F /* User */ = {
- isa = PBXGroup;
- children = (
- C99322B52C986E3A0065E10F /* SelfUserProvider.swift */,
- C99322B62C986E3A0065E10F /* UserModelMappings.swift */,
- C99322B72C986E3A0065E10F /* UserRepository.swift */,
- C97C01D62CC13E1A000683C5 /* UserLocalStore.swift */,
- C99322B82C986E3A0065E10F /* UserRepositoryError.swift */,
- );
- path = User;
- sourceTree = "";
- };
- C99322BC2C986E3A0065E10F /* UpdateEvents */ = {
- isa = PBXGroup;
- children = (
- C99322BA2C986E3A0065E10F /* UpdateEventsRepository.swift */,
- C99322BB2C986E3A0065E10F /* UpdateEventsRepositoryError.swift */,
- );
- path = UpdateEvents;
- sourceTree = "";
- };
- C99322C22C986E3A0065E10F /* FeatureConfig */ = {
- isa = PBXGroup;
- children = (
- C99322BF2C986E3A0065E10F /* FeatureConfigModelMappings.swift */,
- C99322C02C986E3A0065E10F /* FeatureConfigRepository.swift */,
- C99322C12C986E3A0065E10F /* FeatureConfigRepositoryError.swift */,
- );
- path = FeatureConfig;
- sourceTree = "";
- };
- C99322CB2C986E3A0065E10F /* Conversations */ = {
- isa = PBXGroup;
- children = (
- C96B75472CDBA10F003A85EB /* Models */,
- C99322C32C986E3A0065E10F /* ConversationLocalStore.swift */,
- C99322C42C986E3A0065E10F /* ConversationLocalStore+Group.swift */,
- C99322C52C986E3A0065E10F /* ConversationLocalStore+Metadata.swift */,
- C99322C62C986E3A0065E10F /* ConversationLocalStore+MLS.swift */,
- C99322C72C986E3A0065E10F /* ConversationLocalStore+Status.swift */,
- C99322C82C986E3A0065E10F /* ConversationModelMappings.swift */,
- C99322C92C986E3A0065E10F /* ConversationRepository.swift */,
- C99322CA2C986E3A0065E10F /* ConversationRepositoryError.swift */,
- );
- path = Conversations;
- sourceTree = "";
- };
- C99322CE2C986E3A0065E10F /* Connections */ = {
- isa = PBXGroup;
- children = (
- C9FDF3EA2CAA988100D78098 /* ConnectionsModelMappings.swift */,
- C91F111B2C9C4B2B00BA5BE2 /* ConnectionsLocalStore.swift */,
- C91F111A2C9C4B2B00BA5BE2 /* ConnectionsRepository.swift */,
- C99322CD2C986E3A0065E10F /* ConnectionsRepositoryError.swift */,
- );
- path = Connections;
- sourceTree = "";
- };
- C99322D12C986E3A0065E10F /* ConversationsLabels */ = {
- isa = PBXGroup;
- children = (
- C99322CF2C986E3A0065E10F /* ConversationLabelsRepository.swift */,
- C99322D02C986E3A0065E10F /* ConversationLabelsRepositoryError.swift */,
- );
- path = ConversationsLabels;
- sourceTree = "";
- };
- C9C8FDC42C9DBE0E00702B91 /* FeatureConfigEventProcessor */ = {
- isa = PBXGroup;
- children = (
- C9C8FDC32C9DBE0E00702B91 /* FeatureConfigUpdateEventProcessorTests.swift */,
- );
- path = FeatureConfigEventProcessor;
- sourceTree = "";
- };
- C9C8FDC82C9DBE0E00702B91 /* TeamEventProcessor */ = {
- isa = PBXGroup;
- children = (
- C9C8FDC52C9DBE0E00702B91 /* TeamDeleteEventProcessorTests.swift */,
- C9C8FDC62C9DBE0E00702B91 /* TeamMemberLeaveEventProcessorTests.swift */,
- C9C8FDC72C9DBE0E00702B91 /* TeamMemberUpdateEventProcessorTests.swift */,
- );
- path = TeamEventProcessor;
- sourceTree = "";
- };
- C9C8FDCC2C9DBE0E00702B91 /* UserEventProcessor */ = {
- isa = PBXGroup;
- children = (
- C97C01BA2CBE5E65000683C5 /* UserUpdateEventProcessorTests.swift */,
- C97C014F2CB01BDF000683C5 /* OneOnOneResolverTests.swift */,
- C97C01512CB01BEF000683C5 /* UserConnectionEventProcessorTests.swift */,
- C97C015B2CB40038000683C5 /* UserPropertiesSetEventProcessorTests.swift */,
- C97C013C2CAD7D69000683C5 /* UserPropertiesDeleteEventProcessorTests.swift */,
- C9F691282C9B164A008CC41F /* UserPushRemoveEventProcessorTests.swift */,
- C9C8FDC92C9DBE0E00702B91 /* UserClientAddEventProcessorTests.swift */,
- C9C8FDCA2C9DBE0E00702B91 /* UserLegalHoldDisableEventProcessorTests.swift */,
- C9C8FDCB2C9DBE0E00702B91 /* UserLegalholdRequestEventProcessorTests.swift */,
- C97C01AB2CB92D47000683C5 /* UserLegalholdEnableEventProcessorTests.swift */,
- C97C01532CB04626000683C5 /* UserDeleteEventProcessorTests.swift */,
- );
- path = UserEventProcessor;
- sourceTree = "";
- };
- C9C8FDCD2C9DBE0E00702B91 /* Event Processing */ = {
- isa = PBXGroup;
- children = (
- C96B75282CDB7688003A85EB /* ConversationEventProcessor */,
- C97C01582CB40010000683C5 /* FederationEventProcessor */,
- C9C8FDC42C9DBE0E00702B91 /* FeatureConfigEventProcessor */,
- C9C8FDC82C9DBE0E00702B91 /* TeamEventProcessor */,
- C9C8FDCC2C9DBE0E00702B91 /* UserEventProcessor */,
- );
- path = "Event Processing";
- sourceTree = "";
- };
- C9EA76AD2C98548900A7D35C /* UseCases */ = {
- isa = PBXGroup;
- children = (
- C9EA769E2C92DD0F00A7D35C /* PushSupportedProtocolsUseCase.swift */,
- );
- path = UseCases;
- sourceTree = "";
- };
- EE0E117C2C2C4076004BBD29 /* Helpers */ = {
- isa = PBXGroup;
- children = (
- EE0E117D2C2C4080004BBD29 /* TestError.swift */,
- CB7979152C738547006FBA58 /* TestSetup.swift */,
- );
- path = Helpers;
- sourceTree = "";
- };
- EE368CCD2C2DAA87009DBAB0 /* Event Processing */ = {
- isa = PBXGroup;
- children = (
- EE368CCB2C2DAA87009DBAB0 /* UpdateEventProcessor.swift */,
- EEAD09F02C46602300CC8658 /* ConversationEventProcessor */,
- EEAD0A342C46BB7500CC8658 /* FeatureConfigEventProcessor */,
- EEAD0A2F2C46B97700CC8658 /* FederationEventProcessor */,
- EEAD0A0F2C46A32A00CC8658 /* TeamEventProcessor */,
- EEAD0A162C46A87100CC8658 /* UserEventProcessor */,
- );
- path = "Event Processing";
- sourceTree = "";
- };
- EE57A6FC2C298F280096F242 /* Event Decryption */ = {
- isa = PBXGroup;
- children = (
- EE57A6FD2C298F380096F242 /* UpdateEventDecryptor.swift */,
- EE57A6FF2C298F630096F242 /* ProteusMessageDecryptor.swift */,
- EE57A7062C2A86880096F242 /* ProteusMessageDecryptorError.swift */,
- );
- path = "Event Decryption";
- sourceTree = "";
- };
- EE57A7092C2A8B950096F242 /* Event Decryption */ = {
- isa = PBXGroup;
- children = (
- EE57A70A2C2A8BAA0096F242 /* UpdateEventDecryptorTests.swift */,
- EE3F97532C2ADC4C00668DF1 /* ProteusMessageDecryptorTests.swift */,
- );
- path = "Event Decryption";
- sourceTree = "";
- };
- EEAD09F02C46602300CC8658 /* ConversationEventProcessor */ = {
- isa = PBXGroup;
- children = (
- EE368CC72C2DAA87009DBAB0 /* ConversationEventProcessor.swift */,
- EEAD09F12C46604400CC8658 /* ConversationAccessUpdateEventProcessor.swift */,
- EEAD09F32C46709800CC8658 /* ConversationCodeUpdateEventProcessor.swift */,
- EEAD09F52C46772000CC8658 /* ConversationCreateEventProcessor.swift */,
- EEAD09F72C46772C00CC8658 /* ConversationDeleteEventProcessor.swift */,
- EEAD09F92C46773300CC8658 /* ConversationMemberJoinEventProcessor.swift */,
- EEAD09FB2C46773900CC8658 /* ConversationMemberLeaveEventProcessor.swift */,
- EEAD09FD2C46774200CC8658 /* ConversationMemberUpdateEventProcessor.swift */,
- EEAD09FF2C46774900CC8658 /* ConversationMessageTimerUpdateEventProcessor.swift */,
- EEAD0A012C46774F00CC8658 /* ConversationMLSMessageAddEventProcessor.swift */,
- EEAD0A032C46775700CC8658 /* ConversationMLSWelcomeEventProcessor.swift */,
- EEAD0A052C46775D00CC8658 /* ConversationProteusMessageAddEventProcessor.swift */,
- EEAD0A072C46776400CC8658 /* ConversationProtocolUpdateEventProcessor.swift */,
- EEAD0A092C46776B00CC8658 /* ConversationReceiptModeUpdateEventProcessor.swift */,
- EEAD0A0B2C46777200CC8658 /* ConversationRenameEventProcessor.swift */,
- EEAD0A0D2C46777800CC8658 /* ConversationTypingEventProcessor.swift */,
- );
- path = ConversationEventProcessor;
- sourceTree = "";
- };
- EEAD0A0F2C46A32A00CC8658 /* TeamEventProcessor */ = {
- isa = PBXGroup;
- children = (
- EE368CCA2C2DAA87009DBAB0 /* TeamEventProcessor.swift */,
- EEAD0A142C46A3AB00CC8658 /* TeamDeleteEventProcessor.swift */,
- EEAD0A122C46A38200CC8658 /* TeamMemberLeaveEventProcessor.swift */,
- EEAD0A102C46A33E00CC8658 /* TeamMemberUpdateEventProcessor.swift */,
- );
- path = TeamEventProcessor;
- sourceTree = "";
- };
- EEAD0A162C46A87100CC8658 /* UserEventProcessor */ = {
- isa = PBXGroup;
- children = (
- C97C014A2CB00F92000683C5 /* OneOnOneResolver.swift */,
- EE368CCC2C2DAA87009DBAB0 /* UserEventProcessor.swift */,
- EEAD0A172C46A88D00CC8658 /* UserClientAddEventProcessor.swift */,
- EEAD0A192C46A92000CC8658 /* UserClientRemoveEventProcessor.swift */,
- EEAD0A1B2C46A99D00CC8658 /* UserConnectionEventProcessor.swift */,
- EEAD0A1F2C46AB8700CC8658 /* UserDeleteEventProcessor.swift */,
- EEAD0A212C46ABFA00CC8658 /* UserLegalholdDisableEventProcessor.swift */,
- EEAD0A232C46ACEA00CC8658 /* UserLegalholdEnableEventProcessor.swift */,
- EEAD0A252C46AD4D00CC8658 /* UserLegalholdRequestEventProcessor.swift */,
- EEAD0A272C46AE6400CC8658 /* UserPropertiesSetEventProcessor.swift */,
- EEAD0A292C46AEB600CC8658 /* UserPropertiesDeleteEventProcessor.swift */,
- EEAD0A2B2C46AF9200CC8658 /* UserPushRemoveEventProcessor.swift */,
- EEAD0A2D2C46B01900CC8658 /* UserUpdateEventProcessor.swift */,
- );
- path = UserEventProcessor;
- sourceTree = "";
- };
- EEAD0A2F2C46B97700CC8658 /* FederationEventProcessor */ = {
- isa = PBXGroup;
- children = (
- EE368CC92C2DAA87009DBAB0 /* FederationEventProcessor.swift */,
- EEAD0A302C46B98D00CC8658 /* FederationConnectionRemovedEventProcessor.swift */,
- EEAD0A322C46B99800CC8658 /* FederationDeleteEventProcessor.swift */,
- );
- path = FederationEventProcessor;
- sourceTree = "";
- };
- EEAD0A342C46BB7500CC8658 /* FeatureConfigEventProcessor */ = {
- isa = PBXGroup;
- children = (
- EE368CC82C2DAA87009DBAB0 /* FeatureConfigEventProcessor.swift */,
- EEAD0A352C46BBA600CC8658 /* FeatureConfigUpdateEventProcessor.swift */,
- );
- path = FeatureConfigEventProcessor;
- sourceTree = "";
- };
- EEC410252C60D48900E89394 /* Synchronization */ = {
- isa = PBXGroup;
- children = (
- EEC410242C60D48900E89394 /* SyncManagerTests.swift */,
- );
- path = Synchronization;
- sourceTree = "";
- };
- EECC35A42C2EB6C100679448 /* Synchronization */ = {
- isa = PBXGroup;
- children = (
- EECC35A52C2EB6CD00679448 /* SyncManager.swift */,
- EECC35A72C2EB70400679448 /* SyncState.swift */,
- C98433CF2CC26A1D009723D4 /* MLSProvider.swift */,
- );
- path = Synchronization;
- sourceTree = "";
- };
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -889,10 +218,16 @@
);
dependencies = (
);
+ fileSystemSynchronizedGroups = (
+ 59EA7A282D00CFB2002CA0B8 /* WireDomain */,
+ );
name = WireDomain;
packageProductDependencies = (
59909A5D2C5BBEA8009C41DE /* WireAPI */,
598D042C2C89C63100B64D71 /* WireFoundation */,
+ 59EA774E2D00CE0C002CA0B8 /* WireLogging */,
+ 766BE3C42D036D5000148CB6 /* WireDomainAPI */,
+ 594904942D0710BF00238104 /* WireAnalytics */,
);
productName = WireDomain;
productReference = 01D0DCA62C1C8C870076CB1C /* WireDomain.framework */;
@@ -911,6 +246,10 @@
dependencies = (
01D0DCB32C1C8C880076CB1C /* PBXTargetDependency */,
);
+ fileSystemSynchronizedGroups = (
+ 59EA76E82D00CD88002CA0B8 /* TestPlans */,
+ 59EA78992D00CF1C002CA0B8 /* WireDomainTests */,
+ );
name = WireDomainTests;
packageProductDependencies = (
59909A6C2C5BC08B009C41DE /* WireAPISupport */,
@@ -1014,7 +353,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 017F67AF2C20803300B6E02D /* AutoMockable.generated.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1022,85 +360,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- C99322D22C986E3A0065E10F /* TeamRepository.swift in Sources */,
- EEAD0A042C46775700CC8658 /* ConversationMLSWelcomeEventProcessor.swift in Sources */,
- C99322DD2C986E3A0065E10F /* FeatureConfigRepositoryError.swift in Sources */,
- C99322E72C986E3A0065E10F /* ConnectionsRepositoryError.swift in Sources */,
- C98433D02CC26A1D009723D4 /* MLSProvider.swift in Sources */,
- EEAD0A362C46BBA600CC8658 /* FeatureConfigUpdateEventProcessor.swift in Sources */,
- EE57A7002C298F630096F242 /* ProteusMessageDecryptor.swift in Sources */,
- EE368CD02C2DAA87009DBAB0 /* FederationEventProcessor.swift in Sources */,
- EEAD09FA2C46773300CC8658 /* ConversationMemberJoinEventProcessor.swift in Sources */,
- C99322E12C986E3A0065E10F /* ConversationLocalStore+MLS.swift in Sources */,
- C97C014B2CB00F92000683C5 /* OneOnOneResolver.swift in Sources */,
- C99322D52C986E3A0065E10F /* UserModelMappings.swift in Sources */,
- C91F111D2C9C4B2B00BA5BE2 /* ConnectionsLocalStore.swift in Sources */,
- EEAD0A022C46774F00CC8658 /* ConversationMLSMessageAddEventProcessor.swift in Sources */,
- EE368CCE2C2DAA87009DBAB0 /* ConversationEventProcessor.swift in Sources */,
- EEAD0A2C2C46AF9200CC8658 /* UserPushRemoveEventProcessor.swift in Sources */,
0163EE812C20D71C00B37260 /* WireDomain.docc in Sources */,
- EEAD0A2E2C46B01900CC8658 /* UserUpdateEventProcessor.swift in Sources */,
- EEAD0A2A2C46AEB600CC8658 /* UserPropertiesDeleteEventProcessor.swift in Sources */,
- C9FDF3EC2CAA988900D78098 /* ConnectionsModelMappings.swift in Sources */,
- C99322D42C986E3A0065E10F /* SelfUserProvider.swift in Sources */,
- C97C01D72CC13E1A000683C5 /* UserLocalStore.swift in Sources */,
- EEAD0A332C46B99800CC8658 /* FederationDeleteEventProcessor.swift in Sources */,
- C99322E42C986E3A0065E10F /* ConversationRepository.swift in Sources */,
- C99322E02C986E3A0065E10F /* ConversationLocalStore+Metadata.swift in Sources */,
- EEAD0A062C46775D00CC8658 /* ConversationProteusMessageAddEventProcessor.swift in Sources */,
- C91F111C2C9C4B2B00BA5BE2 /* ConnectionsRepository.swift in Sources */,
- EEAD0A282C46AE6400CC8658 /* UserPropertiesSetEventProcessor.swift in Sources */,
- C96B75432CDB8E03003A85EB /* NSManagedObjectContext.swift in Sources */,
- C99322E32C986E3A0065E10F /* ConversationModelMappings.swift in Sources */,
- EEAD09FC2C46773900CC8658 /* ConversationMemberLeaveEventProcessor.swift in Sources */,
- EE57A7082C2A8B740096F242 /* ProteusMessageDecryptorError.swift in Sources */,
- EEAD0A1A2C46A92000CC8658 /* UserClientRemoveEventProcessor.swift in Sources */,
- C98433E92CCA86CA009723D4 /* MessageLocalStore.swift in Sources */,
- EEAD0A0A2C46776B00CC8658 /* ConversationReceiptModeUpdateEventProcessor.swift in Sources */,
- C99322D62C986E3A0065E10F /* UserRepository.swift in Sources */,
- C98433EF2CCB7EE6009723D4 /* MessageType.swift in Sources */,
- EEAD0A002C46774900CC8658 /* ConversationMessageTimerUpdateEventProcessor.swift in Sources */,
- EEAD0A182C46A88D00CC8658 /* UserClientAddEventProcessor.swift in Sources */,
- C99322DC2C986E3A0065E10F /* FeatureConfigRepository.swift in Sources */,
- C99322E22C986E3A0065E10F /* ConversationLocalStore+Status.swift in Sources */,
- EEAD0A0E2C46777800CC8658 /* ConversationTypingEventProcessor.swift in Sources */,
- C99322D92C986E3A0065E10F /* UpdateEventsRepositoryError.swift in Sources */,
- EEAD0A202C46AB8700CC8658 /* UserDeleteEventProcessor.swift in Sources */,
- EEAD0A152C46A3AB00CC8658 /* TeamDeleteEventProcessor.swift in Sources */,
- EEAD0A312C46B98D00CC8658 /* FederationConnectionRemovedEventProcessor.swift in Sources */,
- EEAD09F62C46772000CC8658 /* ConversationCreateEventProcessor.swift in Sources */,
- EEAD0A1C2C46A99D00CC8658 /* UserConnectionEventProcessor.swift in Sources */,
- EEAD09F82C46772C00CC8658 /* ConversationDeleteEventProcessor.swift in Sources */,
- EEAD0A112C46A33E00CC8658 /* TeamMemberUpdateEventProcessor.swift in Sources */,
- C99322E82C986E3A0065E10F /* ConversationLabelsRepository.swift in Sources */,
- C99322E52C986E3A0065E10F /* ConversationRepositoryError.swift in Sources */,
- C98433E72CCA868F009723D4 /* MessageRepository.swift in Sources */,
- C99322DF2C986E3A0065E10F /* ConversationLocalStore+Group.swift in Sources */,
- C99322DE2C986E3A0065E10F /* ConversationLocalStore.swift in Sources */,
- EEAD0A242C46ACEA00CC8658 /* UserLegalholdEnableEventProcessor.swift in Sources */,
- C99322D72C986E3A0065E10F /* UserRepositoryError.swift in Sources */,
- EEAD0A132C46A38200CC8658 /* TeamMemberLeaveEventProcessor.swift in Sources */,
- EEAD0A262C46AD4D00CC8658 /* UserLegalholdRequestEventProcessor.swift in Sources */,
- EEAD09FE2C46774200CC8658 /* ConversationMemberUpdateEventProcessor.swift in Sources */,
- EEAD09F42C46709800CC8658 /* ConversationCodeUpdateEventProcessor.swift in Sources */,
- EE368CCF2C2DAA87009DBAB0 /* FeatureConfigEventProcessor.swift in Sources */,
- EECC35A82C2EB70400679448 /* SyncState.swift in Sources */,
- C99322D82C986E3A0065E10F /* UpdateEventsRepository.swift in Sources */,
- EE368CD12C2DAA87009DBAB0 /* TeamEventProcessor.swift in Sources */,
- C99322D32C986E3A0065E10F /* TeamRepositoryError.swift in Sources */,
- EEAD0A222C46ABFB00CC8658 /* UserLegalholdDisableEventProcessor.swift in Sources */,
- EEAD09F22C46604400CC8658 /* ConversationAccessUpdateEventProcessor.swift in Sources */,
- EEAD0A082C46776400CC8658 /* ConversationProtocolUpdateEventProcessor.swift in Sources */,
- C9EA769F2C92DD0F00A7D35C /* PushSupportedProtocolsUseCase.swift in Sources */,
- EE368CD32C2DAA87009DBAB0 /* UserEventProcessor.swift in Sources */,
- C96B75482CDBA10F003A85EB /* SystemMessage.swift in Sources */,
- EE57A6FE2C298F380096F242 /* UpdateEventDecryptor.swift in Sources */,
- C99322E92C986E3A0065E10F /* ConversationLabelsRepositoryError.swift in Sources */,
- EEAD0A0C2C46777200CC8658 /* ConversationRenameEventProcessor.swift in Sources */,
- C99322DB2C986E3A0065E10F /* FeatureConfigModelMappings.swift in Sources */,
- C97C01A52CB80F54000683C5 /* UserClientsRepository.swift in Sources */,
- EE368CD22C2DAA87009DBAB0 /* UpdateEventProcessor.swift in Sources */,
- EECC35A62C2EB6CD00679448 /* SyncManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1108,48 +368,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- C9F691292C9B164A008CC41F /* UserPushRemoveEventProcessorTests.swift in Sources */,
- C93961932C91B15B00EA971A /* ConversationRepositoryTests.swift in Sources */,
- C96B75292CDB7688003A85EB /* ConversationMemberUpdateEventProcessorTests.swift in Sources */,
- C96B752A2CDB7688003A85EB /* ConversationMemberJoinEventTests.swift in Sources */,
- C96B752B2CDB7688003A85EB /* ConversationProtocolUpdateEventProcessorTests.swift in Sources */,
- C96B752C2CDB7688003A85EB /* ConversationAccessUpdateEventProcessorTests.swift in Sources */,
- C96B752D2CDB7688003A85EB /* ConversationCreateEventProcessorTests.swift in Sources */,
- C96B752E2CDB7688003A85EB /* ConversationReceiptModeUpdateEventProcessorTests.swift in Sources */,
- C96B752F2CDB7688003A85EB /* ConversationMemberLeaveEventTests.swift in Sources */,
- C93961922C91B12800EA971A /* TestError.swift in Sources */,
- EEC410262C60D48900E89394 /* SyncManagerTests.swift in Sources */,
- C9C8FDD32C9DBE0E00702B91 /* UserLegalHoldDisableEventProcessorTests.swift in Sources */,
- EE57A7032C2994420096F242 /* UpdateEventsRepositoryTests.swift in Sources */,
- C97C01522CB01BEF000683C5 /* UserConnectionEventProcessorTests.swift in Sources */,
- C98433F52CCBC290009723D4 /* MessageLocalStoreTests.swift in Sources */,
- C98433F12CCBC251009723D4 /* MessageRepositoryTests.swift in Sources */,
- C9C8FDD02C9DBE0E00702B91 /* TeamMemberLeaveEventProcessorTests.swift in Sources */,
- C97C01A82CB813C9000683C5 /* UserClientsRepositoryTests.swift in Sources */,
- C9E8A3C02C761EDD0093DD5C /* FeatureConfigRepositoryTests.swift in Sources */,
- C9841A1F2CA309310062A834 /* MockFeatureConfigRepositoryProtocol.swift in Sources */,
- C97C015C2CB40038000683C5 /* UserPropertiesSetEventProcessorTests.swift in Sources */,
- CB7979162C738547006FBA58 /* TestSetup.swift in Sources */,
- 162356502C2B223100C6666C /* UserRepositoryTests.swift in Sources */,
- C96B75452CDBA0A9003A85EB /* ConversationDeleteEventProcessorTests.swift in Sources */,
- EE3F97542C2ADC4C00668DF1 /* ProteusMessageDecryptorTests.swift in Sources */,
- EE57A70B2C2A8BAA0096F242 /* UpdateEventDecryptorTests.swift in Sources */,
- C9E8A3AE2C73878B0093DD5C /* ConnectionsRepositoryTests.swift in Sources */,
- C97C015F2CB40EF3000683C5 /* PushSupportedProtocolsUseCaseTests.swift in Sources */,
- C97C01BB2CBE5E65000683C5 /* UserUpdateEventProcessorTests.swift in Sources */,
- C97C013D2CAD7D69000683C5 /* UserPropertiesDeleteEventProcessorTests.swift in Sources */,
- C97C01502CB01BDF000683C5 /* OneOnOneResolverTests.swift in Sources */,
- C9C8FDD22C9DBE0E00702B91 /* UserClientAddEventProcessorTests.swift in Sources */,
- C9C8FDD12C9DBE0E00702B91 /* TeamMemberUpdateEventProcessorTests.swift in Sources */,
- C97C01542CB04626000683C5 /* UserDeleteEventProcessorTests.swift in Sources */,
- C9C8FDCE2C9DBE0E00702B91 /* FeatureConfigUpdateEventProcessorTests.swift in Sources */,
- C9C8FDCF2C9DBE0E00702B91 /* TeamDeleteEventProcessorTests.swift in Sources */,
- C97C01592CB40010000683C5 /* FederationConnectionRemovedEventProcessorTests.swift in Sources */,
- C97C01AC2CB92D47000683C5 /* UserLegalholdEnableEventProcessorTests.swift in Sources */,
- C97C015A2CB40010000683C5 /* FederationDeleteEventProcessorTests.swift in Sources */,
- C9C8FDD42C9DBE0E00702B91 /* UserLegalholdRequestEventProcessorTests.swift in Sources */,
- 017F679C2C20801800B6E02D /* TeamRepositoryTests.swift in Sources */,
- C9E8A3B72C749F2A0093DD5C /* ConversationLabelsRepositoryTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1532,6 +750,10 @@
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
+ 594904942D0710BF00238104 /* WireAnalytics */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = WireAnalytics;
+ };
598D042C2C89C63100B64D71 /* WireFoundation */ = {
isa = XCSwiftPackageProductDependency;
productName = WireFoundation;
@@ -1548,6 +770,14 @@
isa = XCSwiftPackageProductDependency;
productName = WireAPISupport;
};
+ 59EA774E2D00CE0C002CA0B8 /* WireLogging */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = WireLogging;
+ };
+ 766BE3C42D036D5000148CB6 /* WireDomainAPI */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = WireDomainAPI;
+ };
C9E0C9BA2C91B76F00CE6607 /* WireTestingPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = WireTestingPackage;
diff --git a/WireDomain/Project/WireDomain Project.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/WireDomain/Project/WireDomain Project.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 00000000000..08de0be8d3c
--- /dev/null
+++ b/WireDomain/Project/WireDomain Project.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
+
+
+
diff --git a/WireDomain/Project/WireDomain Project.xcodeproj/xcshareddata/xcschemes/WireDomain.xcscheme b/WireDomain/Project/WireDomain Project.xcodeproj/xcshareddata/xcschemes/WireDomain.xcscheme
index 6a736cf7971..aed6939ca90 100644
--- a/WireDomain/Project/WireDomain Project.xcodeproj/xcshareddata/xcschemes/WireDomain.xcscheme
+++ b/WireDomain/Project/WireDomain Project.xcodeproj/xcshareddata/xcschemes/WireDomain.xcscheme
@@ -1,6 +1,6 @@
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/WireDomain/Sources/PackageSupport/Sourcery/sourcery.yml b/WireDomain/Sources/PackageSupport/Sourcery/sourcery.yml
index af8b305c908..11346382dff 100644
--- a/WireDomain/Sources/PackageSupport/Sourcery/sourcery.yml
+++ b/WireDomain/Sources/PackageSupport/Sourcery/sourcery.yml
@@ -1,8 +1,8 @@
sources:
-- ${PACKAGE_ROOT_DIR}/Sources/WireDomain
+- ${PACKAGE_ROOT_DIR}/Sources/Package
templates:
- ${TARGET_DIR}/Sourcery/AutoMockable.stencil
output:
- ${DERIVED_SOURCES_DIR}
+ ${DERIVED_SOURCES_DIR}
args:
- autoMockableImports: ["WireDomainPkg"]
+ autoMockableImports: ["WireDomainPkg"]
diff --git a/WireDomain/Sources/WireDomain/Event Decryption/ProteusMessageDecryptor.swift b/WireDomain/Sources/WireDomain/Event Decryption/ProteusMessageDecryptor.swift
index 0aa46b3bd6c..3dd9b19c7cb 100644
--- a/WireDomain/Sources/WireDomain/Event Decryption/ProteusMessageDecryptor.swift
+++ b/WireDomain/Sources/WireDomain/Event Decryption/ProteusMessageDecryptor.swift
@@ -40,8 +40,6 @@ struct ProteusMessageDecryptor: ProteusMessageDecryptorProtocol {
let proteusService: any ProteusServiceInterface
let managedObjectContext: NSManagedObjectContext
- private let maxCiphertextSize = Int(12_000 * 1.5)
-
typealias Context = (
selfClient: WireDataModel.UserClient,
senderUser: WireDataModel.ZMUser,
@@ -49,14 +47,6 @@ struct ProteusMessageDecryptor: ProteusMessageDecryptorProtocol {
proteusSessionID: ProteusSessionID
)
- init(
- proteusService: any ProteusServiceInterface,
- managedObjectContext: NSManagedObjectContext
- ) {
- self.proteusService = proteusService
- self.managedObjectContext = managedObjectContext
- }
-
func decryptedEventData(
from eventData: ConversationProteusMessageAddEvent
) async throws -> ConversationProteusMessageAddEvent {
@@ -66,11 +56,6 @@ struct ProteusMessageDecryptor: ProteusMessageDecryptorProtocol {
}
let ciphertextData = try validateCiphertext(ciphertext)
-
- if case let .ciphertext(externalCiphertext) = eventData.externalData {
- try validateExternalCiphertext(externalCiphertext)
- }
-
let context = try await extractContext(from: eventData)
let (didCreateSession, plaintextData) = try await proteusService.decrypt(
@@ -95,25 +80,13 @@ struct ProteusMessageDecryptor: ProteusMessageDecryptorProtocol {
throw ProteusMessageDecryptorError.senderFailedToEncrypt
}
- guard
- ciphertext.count <= maxCiphertextSize,
- let ciphertextData = Data(base64Encoded: ciphertext)
- else {
- throw ProteusError.decodeError
+ guard let ciphertextData = Data(base64Encoded: ciphertext) else {
+ throw ProteusMessageDecryptorError.invalidCiphertext
}
return ciphertextData
}
- private func validateExternalCiphertext(_ ciphertext: String) throws {
- // External messages aren't encrypted via Proteus, instead they are symmetrically
- // encrypted with a key that is E2EE via Proteus. Decryption of external messages
- // happens during event processing, here we just want to validate it.
- guard ciphertext.count <= maxCiphertextSize else {
- throw ProteusError.decodeError
- }
- }
-
private func extractContext(
from eventData: ConversationProteusMessageAddEvent
) async throws -> Context {
diff --git a/WireDomain/Sources/WireDomain/Event Decryption/ProteusMessageDecryptorError.swift b/WireDomain/Sources/WireDomain/Event Decryption/ProteusMessageDecryptorError.swift
index 7b5a64dfb23..81b61572845 100644
--- a/WireDomain/Sources/WireDomain/Event Decryption/ProteusMessageDecryptorError.swift
+++ b/WireDomain/Sources/WireDomain/Event Decryption/ProteusMessageDecryptorError.swift
@@ -22,5 +22,6 @@ enum ProteusMessageDecryptorError: Error {
case senderClientNotFound
case proteusSessionIDNotFound
case senderFailedToEncrypt
+ case invalidCiphertext
}
diff --git a/WireDomain/Sources/WireDomain/Event Decryption/UpdateEventDecryptor.swift b/WireDomain/Sources/WireDomain/Event Decryption/UpdateEventDecryptor.swift
index 42a3d8a0a6d..c31935be635 100644
--- a/WireDomain/Sources/WireDomain/Event Decryption/UpdateEventDecryptor.swift
+++ b/WireDomain/Sources/WireDomain/Event Decryption/UpdateEventDecryptor.swift
@@ -18,7 +18,9 @@
import Foundation
import WireAPI
+import WireCoreCrypto
import WireDataModel
+import WireLogging
// sourcery: AutoMockable
/// Decrypt the E2EE content within update events.
@@ -76,8 +78,7 @@ struct UpdateEventDecryptor: UpdateEventDecryptorProtocol {
do {
let decryptedEventData = try await proteusMessageDecryptor.decryptedEventData(from: eventData)
decryptedEvents.append(.conversation(.proteusMessageAdd(decryptedEventData)))
-
- } catch let error as ProteusError {
+ } catch let error as ProteusService.DecryptionError {
WireLogger.updateEvent.error(
"failed to decrypt proteus event payload, dropping: \(error.localizedDescription)",
attributes: logAttributes
@@ -85,7 +86,7 @@ struct UpdateEventDecryptor: UpdateEventDecryptorProtocol {
await appendFailedToDecryptProteusMessage(
eventData: eventData,
- error: error
+ error: error.proteusError
)
} catch {
WireLogger.updateEvent.error(
@@ -108,7 +109,7 @@ struct UpdateEventDecryptor: UpdateEventDecryptorProtocol {
error: ProteusError
) async {
// Do not notify the user if the error is just "duplicated".
- if error == .outdatedMessage || error == .duplicateMessage {
+ if error == .DuplicateMessage {
return
}
@@ -135,7 +136,7 @@ struct UpdateEventDecryptor: UpdateEventDecryptorProtocol {
at: eventData.timestamp,
sender: sender,
client: senderClient,
- errorCode: error.rawValue
+ error: error
)
}
}
diff --git a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationCreateEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationCreateEventProcessor.swift
index 67b4137ce41..3e1ff6af9ff 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationCreateEventProcessor.swift
+++ b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationCreateEventProcessor.swift
@@ -17,6 +17,7 @@
//
import WireAPI
+import WireLogging
import WireSystem
/// Process conversation create events.
@@ -51,7 +52,7 @@ struct ConversationCreateEventProcessor: ConversationCreateEventProcessorProtoco
}
await repository.storeConversation(
- conversation,
+ conversation.toDomainModel(),
timestamp: timestamp
)
}
diff --git a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationEventProcessor.swift
index cf0dd124f34..b2d08dd25c7 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationEventProcessor.swift
+++ b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationEventProcessor.swift
@@ -36,7 +36,6 @@ protocol ConversationEventProcessorProtocol {
struct ConversationEventProcessor {
let accessUpdateEventProcessor: any ConversationAccessUpdateEventProcessorProtocol
- let codeUpdateEventProcessor: any ConversationCodeUpdateEventProcessorProtocol
let createEventProcessor: any ConversationCreateEventProcessorProtocol
let deleteEventProcessor: any ConversationDeleteEventProcessorProtocol
let memberJoinEventProcessor: any ConversationMemberJoinEventProcessorProtocol
@@ -54,13 +53,15 @@ struct ConversationEventProcessor {
func processEvent(_ event: ConversationEvent) async throws {
switch event {
case let .accessUpdate(event):
- try await accessUpdateEventProcessor.processEvent(event)
+ await accessUpdateEventProcessor.processEvent(event)
- case let .codeUpdate(event):
- try await codeUpdateEventProcessor.processEvent(event)
+ case .codeUpdate:
+ // Event is not currently processed instead we fetch guest link on demand directly from API, see
+ // `CreateConversationGuestLinkUseCase` and `CreateConversationGuestLinkActionHandler`
+ break
case let .create(event):
- try await createEventProcessor.processEvent(event)
+ await createEventProcessor.processEvent(event)
case let .delete(event):
try await deleteEventProcessor.processEvent(event)
@@ -75,7 +76,7 @@ struct ConversationEventProcessor {
try await memberUpdateEventProcessor.processEvent(event)
case let .messageTimerUpdate(event):
- try await messageTimerUpdateEventProcessor.processEvent(event)
+ await messageTimerUpdateEventProcessor.processEvent(event)
case let .mlsMessageAdd(event):
try await mlsMessageAddEventProcessor.processEvent(event)
diff --git a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationMLSWelcomeEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationMLSWelcomeEventProcessor.swift
index 78ed4a18d38..4d36f874de4 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationMLSWelcomeEventProcessor.swift
+++ b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationMLSWelcomeEventProcessor.swift
@@ -17,6 +17,8 @@
//
import WireAPI
+import WireDataModel
+import WireLogging
/// Process conversation mls welcome events.
@@ -32,9 +34,77 @@ protocol ConversationMLSWelcomeEventProcessorProtocol {
struct ConversationMLSWelcomeEventProcessor: ConversationMLSWelcomeEventProcessorProtocol {
- func processEvent(_: ConversationMLSWelcomeEvent) async throws {
- // TODO: [WPB-10173]
- assertionFailure("not implemented yet")
+ enum Failure: Error {
+ case conversationNotFound
}
+ let conversationRepository: any ConversationRepositoryProtocol
+ let conversationLocalStore: any ConversationLocalStoreProtocol
+ let mlsService: any MLSServiceInterface
+ let mlsDecryptionService: any MLSDecryptionServiceInterface
+ let oneOnOneResolver: any OneOnOneResolverProtocol
+
+ func processEvent(_ event: ConversationMLSWelcomeEvent) async throws {
+ let welcomeMessage = event.welcomeMessage
+ let conversationID = event.conversationID
+
+ WireLogger.mls.info("MLS event processor is processing welcome message")
+
+ // Decrypts the welcome message which returns the group ID of the conversation we were added to.
+ let groupID = try await mlsDecryptionService.processWelcomeMessage(
+ welcomeMessage: welcomeMessage
+ )
+
+ var conversation = await conversationRepository.fetchConversation(
+ id: conversationID.uuid,
+ domain: conversationID.domain
+ )
+
+ if conversation == nil {
+ // sync conversation with backend
+ try await conversationRepository.pullConversation(
+ id: conversationID.uuid,
+ domain: conversationID.domain
+ )
+
+ conversation = await conversationRepository.fetchConversation(
+ id: conversationID.uuid,
+ domain: conversationID.domain
+ )
+ }
+
+ guard let conversation else {
+ throw Failure.conversationNotFound
+ }
+
+ // This conversation is now a MLS one so we need to update its group ID and set MLS status to ready..
+ await conversationLocalStore.storeMLSConversationEstablished(
+ mlsGroupID: groupID,
+ conversation: conversation
+ )
+
+ // ..and also update/create the related MLS group.
+ await conversationLocalStore.updateOrCreateMLSGroup(
+ groupID: groupID
+ )
+
+ // Ensures we have MLS valid key packages published otherwise the user can’t be added to any new groups.
+ await mlsService.uploadKeyPackagesIfNeeded()
+
+ do {
+ // We need to resolve the now MLS 1:1 conversation with the other user
+ let otherUserQualifiedID = await conversationLocalStore.fetchOtherUserIDInOneOnOneConversation(
+ conversation: conversation
+ )
+
+ guard let otherUserQualifiedID else {
+ return
+ }
+
+ try await oneOnOneResolver.resolveOneOnOneConversation(with: otherUserQualifiedID)
+ WireLogger.mls.debug("successfully resolved one on one conversation")
+ } catch {
+ WireLogger.mls.warn("failed to resolve one on one conversation: \(error)")
+ }
+ }
}
diff --git a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationMessageTimerUpdateEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationMessageTimerUpdateEventProcessor.swift
index f121db3fad4..6e46afc521c 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationMessageTimerUpdateEventProcessor.swift
+++ b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationMessageTimerUpdateEventProcessor.swift
@@ -17,6 +17,7 @@
//
import WireAPI
+import WireDataModel
/// Process conversation message timer update events.
@@ -26,15 +27,49 @@ protocol ConversationMessageTimerUpdateEventProcessorProtocol {
///
/// - Parameter event: A conversation message timer update event.
- func processEvent(_ event: ConversationMessageTimerUpdateEvent) async throws
+ func processEvent(_ event: ConversationMessageTimerUpdateEvent) async
}
struct ConversationMessageTimerUpdateEventProcessor: ConversationMessageTimerUpdateEventProcessorProtocol {
- func processEvent(_: ConversationMessageTimerUpdateEvent) async throws {
- // TODO: [WPB-10171]
- assertionFailure("not implemented yet")
+ let conversationLocalStore: any ConversationLocalStoreProtocol
+ let messageLocalStore: any MessageLocalStoreProtocol
+
+ func processEvent(_ event: ConversationMessageTimerUpdateEvent) async {
+ let userID = event.senderID
+ let conversationID = event.conversationID
+ let timerInMilliseconds = Double(event.newTimer ?? 0)
+ let timestamp = event.timestamp
+
+ let conversation = await conversationLocalStore.fetchOrCreateConversation(
+ id: conversationID.uuid,
+ domain: conversationID.domain
+ )
+
+ let timeoutValue = timerInMilliseconds / 1000
+ let timeout: MessageDestructionTimeoutValue = .init(rawValue: timeoutValue)
+ let currentTimeout = await conversationLocalStore.conversationMessageDestructionTimeout(conversation)
+
+ if currentTimeout != timeout {
+
+ let messageType: MessageType = .messageTimerUpdate(
+ sender: (userID.uuid, userID.domain),
+ date: timestamp,
+ timeoutValue: timeoutValue
+ )
+
+ await messageLocalStore.addSystemMessageToConversation(
+ messageType: messageType,
+ conversationID: conversationID.uuid,
+ conversationDomain: conversationID.domain
+ )
+ }
+
+ await conversationLocalStore.storeConversation(
+ timeoutValue: timeoutValue,
+ for: conversation
+ )
}
}
diff --git a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessor.swift
index 7ef16b24f3e..1e2a473d938 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessor.swift
+++ b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessor.swift
@@ -17,6 +17,7 @@
//
import WireAPI
+import WireLogging
import WireSystem
/// Process conversation receipt mode update events.
@@ -36,11 +37,12 @@ struct ConversationReceiptModeUpdateEventProcessor: ConversationReceiptModeUpdat
let userRepository: any UserRepositoryProtocol
let conversationRepository: any ConversationRepositoryProtocol
let conversationLocalStore: any ConversationLocalStoreProtocol
+ let messageRepository: any MessageRepositoryProtocol
func processEvent(_ event: ConversationReceiptModeUpdateEvent) async throws {
let senderID = event.senderID
let conversationID = event.conversationID
- let isEnabled = event.newRecieptMode == 1
+ let isEnabled = event.newReceiptMode == 1
let sender = try await userRepository.fetchUser(
id: senderID.uuid,
@@ -69,20 +71,17 @@ struct ConversationReceiptModeUpdateEventProcessor: ConversationReceiptModeUpdat
timestamp: .now
)
- await conversationRepository.addSystemMessage(
- systemMessage,
- to: conversation
+ let systemMessageType: MessageType = .readReceiptsStatus(
+ isEnabled: isEnabled,
+ sender: (senderID.uuid, senderID.domain),
+ date: .now
)
- let isConversationArchived = await conversationLocalStore.isConversationArchived(conversation)
- let mutedMessageTypes = await conversationLocalStore.conversationMutedMessageTypes(conversation)
-
- if isConversationArchived, mutedMessageTypes == .none {
- await conversationLocalStore.storeConversation(
- isArchived: false,
- for: conversation
- )
- }
+ await messageRepository.addMessageToConversation(
+ messageType: systemMessageType,
+ conversationID: conversationID.uuid,
+ conversationDomain: conversationID.domain
+ )
}
}
diff --git a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationRenameEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationRenameEventProcessor.swift
index 1dad0b00399..6d9f27c2a39 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationRenameEventProcessor.swift
+++ b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationRenameEventProcessor.swift
@@ -32,9 +32,22 @@ protocol ConversationRenameEventProcessorProtocol {
struct ConversationRenameEventProcessor: ConversationRenameEventProcessorProtocol {
- func processEvent(_: ConversationRenameEvent) async throws {
- // TODO: [WPB-10177]
- assertionFailure("not implemented yet")
+ let repository: any ConversationRepositoryProtocol
+
+ func processEvent(_ event: ConversationRenameEvent) async throws {
+ let newName = event.newName
+ let conversationID = event.conversationID
+ let senderID = event.senderID
+ let timestamp = event.timestamp
+
+ await repository.updateConversationName(
+ newName: newName,
+ conversationID: conversationID.uuid,
+ conversationDomain: conversationID.domain,
+ senderID: senderID.uuid,
+ senderDomain: senderID.domain,
+ date: timestamp
+ )
}
}
diff --git a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessor/ConversationTypingEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessor/ConversationTypingEventProcessor.swift
new file mode 100644
index 00000000000..bd6fb200e56
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessor/ConversationTypingEventProcessor.swift
@@ -0,0 +1,131 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireAPI
+import WireDataModel
+
+/// Process conversation typing events.
+
+protocol ConversationTypingEventProcessorProtocol {
+
+ /// Process a conversation typing event.
+ ///
+ /// - Parameter event: A conversation typing event.
+
+ func processEvent(_ event: ConversationTypingEvent) async
+
+}
+
+struct ConversationTypingEventProcessor: ConversationTypingEventProcessorProtocol {
+
+ let conversationRepository: any ConversationRepositoryProtocol
+ let conversationLocalStore: any ConversationLocalStoreProtocol
+ let userRepository: any UserRepositoryProtocol
+
+ private let typingUsersTimeout = ConversationTypingUsersTimeout()
+
+ func processEvent(_ event: ConversationTypingEvent) async {
+ let conversationID = event.conversationID
+ let senderID = event.senderID
+ let isTyping = event.isTyping
+
+ let user = await userRepository.fetchOrCreateUser(
+ id: senderID.uuid,
+ domain: senderID.domain
+ )
+
+ let conversation = await conversationRepository.fetchOrCreateConversation(
+ id: conversationID.uuid,
+ domain: conversationID.domain
+ )
+
+ // Since we'll be manipulating managed object IDs in `ConversationTypingUsersTimeout`
+ // we need to make sure we have valid, consistent IDs for the user and conversation.
+ conversationLocalStore.obtainPermanentIDs(
+ user: user,
+ conversation: conversation
+ )
+
+ let userObjectID = user.objectID
+ let conversationObjectID = conversation.objectID
+
+ let wasTyping = typingUsersTimeout.contains(
+ userObjectID,
+ for: conversationObjectID
+ )
+
+ if isTyping {
+ let timeout = ConversationTypingUsersTimeout.defaultTimeout // 60 sec
+
+ // Tracks the typing user timeout for a given conversation
+ typingUsersTimeout.add(
+ userObjectID,
+ for: conversationObjectID,
+ withTimeout: Date(timeIntervalSinceNow: timeout)
+ )
+ }
+
+ // Typing status changed
+ if wasTyping != isTyping {
+ if !isTyping {
+ // User is no longer typing, untracking him
+ typingUsersTimeout.remove(
+ userObjectID,
+ for: conversationObjectID
+ )
+ }
+
+ let userObjectIDs = typingUsersTimeout.userIds(
+ in: conversationObjectID
+ )
+
+ let typingUsersInfo = ConversationTypingUsersInfo(
+ users: userObjectIDs,
+ conversationID: conversationObjectID
+ )
+
+ // Updates non timed out typing users
+ await conversationRepository.updateTypingUsers([typingUsersInfo])
+ }
+
+ typingUsersTimeout.timerFiredCallback = timerDidFire
+
+ typingUsersTimeout.updateExpirationIfNeeded()
+ }
+
+ /// Called when timer from `ConversationTypingUsersTimeout` fires.
+ /// Untrack timed out typing users and updates valid (non timed out) typing users.
+ /// Callback is fired when a new timeout value is reached in `timeouts` dictionary.
+
+ private func timerDidFire() async {
+ let conversationObjectIDs = typingUsersTimeout.pruneConversationsThatHaveTimoutBefore(
+ date: .now
+ )
+
+ // Map typing users for each conversation
+ let typingUsersInfo: [ConversationTypingUsersInfo] = conversationObjectIDs.map {
+ let userObjectIDs = typingUsersTimeout.userIds(in: $0)
+ return .init(users: userObjectIDs, conversationID: $0)
+ }
+
+ await conversationRepository.updateTypingUsers(typingUsersInfo)
+
+ typingUsersTimeout.updateExpirationIfNeeded()
+ }
+
+}
diff --git a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessor/ConversationTypingUsersInfo.swift b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessor/ConversationTypingUsersInfo.swift
new file mode 100644
index 00000000000..3043facbe5b
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessor/ConversationTypingUsersInfo.swift
@@ -0,0 +1,25 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import CoreData
+
+/// The typing users for a given conversation
+public struct ConversationTypingUsersInfo {
+ let users: Set
+ let conversationID: NSManagedObjectID
+}
diff --git a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessor/ConversationTypingUsersTimeout.swift b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessor/ConversationTypingUsersTimeout.swift
new file mode 100644
index 00000000000..216f8e27b29
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessor/ConversationTypingUsersTimeout.swift
@@ -0,0 +1,117 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+
+/// Keeping track of typing users timeouts
+final class ConversationTypingUsersTimeout: NSObject, ZMTimerClient {
+
+ struct Key: Hashable {
+ let userObjectID: NSManagedObjectID
+ let conversationObjectID: NSManagedObjectID
+ }
+
+ static let defaultTimeout: TimeInterval = 60
+
+ private var timeouts = [Key: Date]()
+ private var nextPruneDate: Date?
+ private var expirationTimer: ZMTimer?
+
+ typealias VoidClosure = () async -> Void
+ var timerFiredCallback: VoidClosure?
+
+ var firstTimeout: Date? {
+ timeouts.values.min()
+ }
+
+ func add(
+ _ user: NSManagedObjectID,
+ for conversation: NSManagedObjectID,
+ withTimeout timeout: Date
+ ) {
+ let key = Key(userObjectID: user, conversationObjectID: conversation)
+ timeouts[key] = timeout
+ }
+
+ func remove(
+ _ user: NSManagedObjectID,
+ for conversation: NSManagedObjectID
+ ) {
+ let key = Key(userObjectID: user, conversationObjectID: conversation)
+ timeouts.removeValue(forKey: key)
+ }
+
+ func contains(
+ _ user: NSManagedObjectID,
+ for conversation: NSManagedObjectID
+ ) -> Bool {
+ let key = Key(userObjectID: user, conversationObjectID: conversation)
+ return timeouts[key] != nil
+ }
+
+ func userIds(
+ in conversation: NSManagedObjectID
+ ) -> Set {
+ let userIds = timeouts.keys
+ .filter { $0.conversationObjectID == conversation }
+ .map(\.userObjectID)
+
+ return Set(userIds)
+ }
+
+ func pruneConversationsThatHaveTimoutBefore(
+ date pruneDate: Date
+ ) -> Set {
+ let keysToRemove = timeouts
+ .filter { $0.value < pruneDate }
+ .keys
+
+ keysToRemove.forEach {
+ timeouts.removeValue(forKey: $0)
+ }
+
+ return Set(keysToRemove.map(\.conversationObjectID))
+ }
+
+ /// Updates next prune date and fire a timer at that specific date.
+ /// When timer fires, the provided callback will be called - see `timerDidFire`
+
+ func updateExpirationIfNeeded() {
+ guard
+ let date = firstTimeout,
+ date != nextPruneDate
+ else {
+ return
+ }
+
+ expirationTimer?.cancel()
+ expirationTimer = ZMTimer(target: self)
+ expirationTimer?.fire(at: date)
+ nextPruneDate = date
+ }
+
+ func timerDidFire(_ timer: ZMTimer!) {
+ guard timer === expirationTimer else {
+ return
+ }
+
+ Task { [timerFiredCallback] in
+ await timerFiredCallback?()
+ }
+ }
+}
diff --git a/WireDomain/Sources/WireDomain/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessor.swift
index ef7f131dd05..2f4ea790162 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessor.swift
+++ b/WireDomain/Sources/WireDomain/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessor.swift
@@ -37,9 +37,9 @@ struct TeamMemberLeaveEventProcessor: TeamMemberLeaveEventProcessorProtocol {
func processEvent(_ event: TeamMemberLeaveEvent) async throws {
try await repository.deleteMembership(
- for: event.userID,
+ userID: event.userID,
domain: nil,
- at: event.time
+ date: event.time
)
}
diff --git a/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserClientAddEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserClientAddEventProcessor.swift
index 90e0f675c18..15e6b6b95ae 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserClientAddEventProcessor.swift
+++ b/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserClientAddEventProcessor.swift
@@ -41,11 +41,11 @@ struct UserClientAddEventProcessor: UserClientAddEventProcessorProtocol {
func processEvent(_ event: UserClientAddEvent) async throws {
do {
let localUserClient = try await repository.fetchOrCreateClient(
- with: event.client.id
+ id: event.client.id
)
try await repository.updateClient(
- with: event.client.id,
+ id: event.client.id,
from: event.client,
isNewClient: localUserClient.isNew
)
diff --git a/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserConnectionEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserConnectionEventProcessor.swift
index 2d50ea45361..40cf367e47c 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserConnectionEventProcessor.swift
+++ b/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserConnectionEventProcessor.swift
@@ -42,7 +42,7 @@ struct UserConnectionEventProcessor: UserConnectionEventProcessorProtocol {
try await connectionsRepository.updateConnection(connection)
if connection.status == .accepted {
- try await oneOnOneResolver.invoke()
+ try await oneOnOneResolver.resolveAllOneOnOneConversations()
}
}
}
diff --git a/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserEventProcessor.swift
index ccae8373400..894e1513067 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserEventProcessor.swift
+++ b/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserEventProcessor.swift
@@ -59,7 +59,7 @@ struct UserEventProcessor {
case let .connection(event):
try await connectionEventProcessor.processEvent(event)
- case let .contactJoin(event):
+ case .contactJoin:
/// This event is not processed, we only show a notification to the user.
break
@@ -79,7 +79,7 @@ struct UserEventProcessor {
try await propertiesSetEventProcessor.processEvent(event)
case let .propertiesDelete(event):
- try await propertiesDeleteEventProcessor.processEvent(event)
+ await propertiesDeleteEventProcessor.processEvent(event)
case .pushRemove:
pushRemoveEventProcessor.processEvent()
diff --git a/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserPropertiesDeleteEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserPropertiesDeleteEventProcessor.swift
index 26272f0d291..df38a0613ad 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserPropertiesDeleteEventProcessor.swift
+++ b/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserPropertiesDeleteEventProcessor.swift
@@ -17,6 +17,7 @@
//
import WireAPI
+import WireLogging
import WireSystem
/// Process user properties delete events.
diff --git a/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsLocalStore.swift
index f93935b752b..4351a15a002 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsLocalStore.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsLocalStore.swift
@@ -16,15 +16,16 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import CoreData
-import Foundation
-import WireAPI
import WireDataModel
-protocol ConnectionsLocalStoreProtocol {
+// sourcery: AutoMockable
+public protocol ConnectionsLocalStoreProtocol {
+
+ /// Save connection and related objects to local storage.
+ /// - Parameter connectionInfo: connection object
func storeConnection(
- _ connectionPayload: Connection
+ _ connectionInfo: ConnectionInfo
) async throws
}
@@ -44,15 +45,12 @@ final class ConnectionsLocalStore: ConnectionsLocalStoreProtocol {
// MARK: - Public
- /// Save connection and related objects to local storage.
- /// - Parameter connectionPayload: connection object from WireAPI
-
- public func storeConnection(_ connectionPayload: Connection) async throws {
+ public func storeConnection(_ connectionInfo: ConnectionInfo) async throws {
try await context.perform { [self] in
- let connection = try storedConnection(from: connectionPayload)
+ let connection = try storedConnection(from: connectionInfo)
- let conversation = try storedConversation(from: connectionPayload, with: connection)
+ let conversation = try storedConversation(from: connectionInfo, with: connection)
connection.to.oneOnOneConversation = conversation
@@ -67,7 +65,7 @@ final class ConnectionsLocalStore: ConnectionsLocalStoreProtocol {
/// - Returns: conversation object stored locally
private func storedConversation(
- from connection: Connection,
+ from connection: ConnectionInfo,
with storedConnection: ZMConnection
) throws -> ZMConversation {
guard let conversationID = connection.conversationID ?? connection.qualifiedConversationID?.uuid else {
@@ -90,7 +88,9 @@ final class ConnectionsLocalStore: ConnectionsLocalStoreProtocol {
/// - Parameter connection: connection payload from WireAPI
/// - Returns: connection object stored locally
- private func storedConnection(from connection: Connection) throws -> ZMConnection {
+ private func storedConnection(
+ from connection: ConnectionInfo
+ ) throws -> ZMConnection {
guard let userID = connection.receiverID ?? connection.receiverQualifiedID?.uuid else {
throw ConnectionsRepositoryError.missingReceiverId
}
@@ -101,7 +101,7 @@ final class ConnectionsLocalStore: ConnectionsLocalStoreProtocol {
in: context
)
- storedConnection.status = connection.status.toDomainModel()
+ storedConnection.status = connection.status
storedConnection.lastUpdateDateInGMT = connection.lastUpdate
return storedConnection
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsModelMappings.swift b/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsModelMappings.swift
index f9a778101a0..78fe079a49f 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsModelMappings.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsModelMappings.swift
@@ -41,3 +41,18 @@ extension WireAPI.ConnectionStatus {
}
}
+
+extension WireAPI.Connection {
+
+ func toDomainModel() -> ConnectionInfo {
+ .init(
+ senderID: senderID,
+ receiverID: receiverID,
+ receiverQualifiedID: receiverQualifiedID?.toDomainModel(),
+ conversationID: conversationID,
+ qualifiedConversationID: qualifiedConversationID?.toDomainModel(),
+ lastUpdate: lastUpdate,
+ status: status.toDomainModel()
+ )
+ }
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsRepository.swift b/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsRepository.swift
index be089b4c7c6..07ccf94aa7c 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsRepository.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsRepository.swift
@@ -70,7 +70,7 @@ public struct ConnectionsRepository: ConnectionsRepositoryProtocol {
await withThrowingTaskGroup(of: Void.self) { taskGroup in
for connection in connections {
taskGroup.addTask {
- try await connectionsLocalStore.storeConnection(connection)
+ try await connectionsLocalStore.storeConnection(connection.toDomainModel())
}
}
}
@@ -80,7 +80,7 @@ public struct ConnectionsRepository: ConnectionsRepositoryProtocol {
public func updateConnection(
_ connection: Connection
) async throws {
- try await connectionsLocalStore.storeConnection(connection)
+ try await connectionsLocalStore.storeConnection(connection.toDomainModel())
}
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Connections/Models/ConnectionInfo.swift b/WireDomain/Sources/WireDomain/Repositories/Connections/Models/ConnectionInfo.swift
new file mode 100644
index 00000000000..4b644cb7e35
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/Connections/Models/ConnectionInfo.swift
@@ -0,0 +1,29 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+
+public struct ConnectionInfo: Sendable {
+ public let senderID: UUID?
+ public let receiverID: UUID?
+ public let receiverQualifiedID: WireDataModel.QualifiedID?
+ public let conversationID: UUID?
+ public let qualifiedConversationID: WireDataModel.QualifiedID?
+ public let lastUpdate: Date
+ public let status: WireDataModel.ZMConnectionStatus
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+Group.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+Group.swift
index e4ce2049149..ea1841b4e06 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+Group.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+Group.swift
@@ -26,16 +26,16 @@ extension ConversationLocalStore {
// MARK: - User & Role
func fetchUserAndRole(
- from remoteConversationMember: WireAPI.Conversation.Member,
+ from conversationMember: Conversation.Members.Member,
for localConversation: ZMConversation
) -> (user: ZMUser, role: Role?)? {
- guard let userID = remoteConversationMember.id ?? remoteConversationMember.qualifiedID?.uuid else {
+ guard let userID = conversationMember.id ?? conversationMember.qualifiedID?.uuid else {
return nil
}
let user = ZMUser.fetchOrCreate(
with: userID,
- domain: remoteConversationMember.qualifiedID?.domain,
+ domain: conversationMember.qualifiedID?.domain,
in: context
)
@@ -50,8 +50,11 @@ extension ConversationLocalStore {
)
}
- let role = remoteConversationMember.conversationRole.map {
- fetchOrCreateRoleForConversation(name: $0, conversation: localConversation)
+ let role = conversationMember.conversationRole.map {
+ fetchOrCreateRoleForConversation(
+ name: $0,
+ conversation: localConversation
+ )
}
return (user, role)
@@ -60,10 +63,10 @@ extension ConversationLocalStore {
// MARK: - Members
func updateMembers(
- from remoteConversation: WireAPI.Conversation,
+ from conversation: Conversation,
for localConversation: ZMConversation
) {
- guard let members = remoteConversation.members else {
+ guard let members = conversation.members else {
return
}
@@ -79,7 +82,10 @@ extension ConversationLocalStore {
for: localConversation
)?.role
- localConversation.updateMembers(otherMembers, selfUserRole: selfUserRole)
+ localConversation.updateMembers(
+ otherMembers,
+ selfUserRole: selfUserRole
+ )
}
// MARK: - 1:1
diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+MLS.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+MLS.swift
index 36a7c3aa5ee..0531f14c025 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+MLS.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+MLS.swift
@@ -26,24 +26,24 @@ extension ConversationLocalStore {
// MARK: - Message protocols
func assignMessageProtocol(
- from remoteConversation: WireAPI.Conversation,
+ from conversation: Conversation,
for localConversation: ZMConversation
) {
- guard let newMessageProtocol = remoteConversation.messageProtocol else {
+ guard let newMessageProtocol = conversation.messageProtocol else {
eventProcessingLogger.warn(
"message protocol is missing"
)
return
}
- localConversation.messageProtocol = newMessageProtocol.toDomainModel()
+ localConversation.messageProtocol = newMessageProtocol
}
func updateMessageProtocol(
- from remoteConversation: WireAPI.Conversation,
+ from conversation: Conversation,
for localConversation: ZMConversation
) {
- guard let newMessageProtocol = remoteConversation.messageProtocol else {
+ guard let newMessageProtocol = conversation.messageProtocol else {
eventProcessingLogger.warn(
"message protocol is missing"
)
@@ -59,12 +59,16 @@ extension ConversationLocalStore {
break /// no update, ignore
case .mixed:
localConversation.appendMLSMigrationStartedSystemMessage(sender: sender, at: .now)
- localConversation.messageProtocol = newMessageProtocol.toDomainModel()
+ localConversation.messageProtocol = newMessageProtocol
case .mls:
let date = localConversation.lastModifiedDate ?? .now
- localConversation.appendMLSMigrationPotentialGapSystemMessage(sender: sender, at: date)
- localConversation.messageProtocol = newMessageProtocol.toDomainModel()
+
+ localConversation.appendMLSMigrationPotentialGapSystemMessage(
+ sender: sender, at: date
+ )
+
+ localConversation.messageProtocol = newMessageProtocol
}
case .mixed:
@@ -78,7 +82,7 @@ extension ConversationLocalStore {
break /// no update, ignore
case .mls:
localConversation.appendMLSMigrationFinalizedSystemMessage(sender: sender, at: .now)
- localConversation.messageProtocol = newMessageProtocol.toDomainModel()
+ localConversation.messageProtocol = newMessageProtocol
}
case .mls:
@@ -119,8 +123,14 @@ extension ConversationLocalStore {
)
if await context.perform({ localConversation.epoch <= 0 }) {
- let ciphersuite = try await mlsService.createSelfGroup(for: groupID)
- await context.perform { localConversation.ciphersuite = ciphersuite }
+ let ciphersuite = try await mlsService.createSelfGroup(
+ for: groupID
+ )
+
+ await context.perform {
+ localConversation.ciphersuite = ciphersuite
+ }
+
} else if try await !mlsService.conversationExists(groupID: groupID) {
try await mlsService.joinGroup(with: groupID)
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+Metadata.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+Metadata.swift
index f669ac6c9a1..99ea4375ad5 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+Metadata.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+Metadata.swift
@@ -26,25 +26,25 @@ extension ConversationLocalStore {
// MARK: - Metadata
func updateMetadata(
- from remoteConversation: WireAPI.Conversation,
+ from conversation: Conversation,
for localConversation: ZMConversation
) {
- if let teamID = remoteConversation.teamID {
+ if let teamID = conversation.teamID {
localConversation.updateTeam(identifier: teamID)
}
- if let name = remoteConversation.name {
+ if let name = conversation.name {
localConversation.userDefinedName = name
}
- guard let userID = remoteConversation.creator else {
+ guard let userID = conversation.creator else {
return
}
/// We assume that the creator always belongs to the same domain as the conversation
let creator = ZMUser.fetchOrCreate(
with: userID,
- domain: remoteConversation.qualifiedID?.domain,
+ domain: conversation.qualifiedID?.domain,
in: context
)
@@ -54,28 +54,28 @@ extension ConversationLocalStore {
// MARK: - Attributes
func updateAttributes(
- from remoteConversation: WireAPI.Conversation,
+ from conversation: Conversation,
for localConversation: ZMConversation,
isFederationEnabled: Bool
) {
- localConversation.domain = isFederationEnabled ? remoteConversation.qualifiedID?.domain : nil
+ localConversation.domain = isFederationEnabled ? conversation.qualifiedID?.domain : nil
localConversation.needsToBeUpdatedFromBackend = false
- if let epoch = remoteConversation.epoch {
+ if let epoch = conversation.epoch {
localConversation.epoch = UInt64(epoch)
}
- let base64String = remoteConversation.mlsGroupID
+ let base64String = conversation.mlsGroupID
if let base64String, let mlsGroupID = MLSGroupID(base64Encoded: base64String) {
localConversation.mlsGroupID = mlsGroupID
}
- let ciphersuite = remoteConversation.cipherSuite
- let epoch = remoteConversation.epoch
+ let ciphersuite = conversation.cipherSuite
+ let epoch = conversation.epoch
if let ciphersuite, let epoch, epoch > 0 {
- localConversation.ciphersuite = ciphersuite.toDomainModel()
+ localConversation.ciphersuite = ciphersuite
}
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+Status.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+Status.swift
index 7e4b72f6359..2c578a70b74 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+Status.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore+Status.swift
@@ -26,7 +26,7 @@ extension ConversationLocalStore {
// MARK: - Conversation status
func updateConversationStatus(
- from remoteConversation: WireAPI.Conversation,
+ from remoteConversation: Conversation,
for localConversation: ZMConversation
) {
if let readReceiptMode = remoteConversation.readReceiptMode {
@@ -36,13 +36,13 @@ extension ConversationLocalStore {
if let accessModes = remoteConversation.access {
if let accessRoles = remoteConversation.accessRoles {
localConversation.updateAccessStatus(
- accessModes: accessModes.map(\.rawValue),
- accessRoles: accessRoles.map(\.rawValue)
+ accessModes: accessModes,
+ accessRoles: accessRoles
)
} else if let accessRole = remoteConversation.legacyAccessRole {
- let accessRoles = ConversationAccessRoleV2.fromLegacyAccessRole(accessRole.toDomainModel())
+ let accessRoles = ConversationAccessRoleV2.fromLegacyAccessRole(accessRole)
localConversation.updateAccessStatus(
- accessModes: accessModes.map(\.rawValue),
+ accessModes: accessModes,
accessRoles: accessRoles.map(\.rawValue)
)
}
@@ -56,14 +56,17 @@ extension ConversationLocalStore {
// MARK: - MLS status
func updateMLSStatus(
- from remoteConversation: WireAPI.Conversation,
- for localConversation: ZMConversation
+ from remoteConversation: Conversation,
+ for localConversation: ZMConversation,
+ isMLSEnabled: Bool
) async {
- guard DeveloperFlag.enableMLSSupport.isOn else { return }
+ guard isMLSEnabled else { return }
await updateConversationIfNeeded(
localConversation: localConversation,
- fallbackGroupID: remoteConversation.mlsGroupID.map { .init(base64Encoded: $0) } ?? nil
+ fallbackGroupID: remoteConversation.mlsGroupID.map {
+ .init(base64Encoded: $0)
+ } ?? nil
)
}
@@ -96,7 +99,9 @@ extension ConversationLocalStore {
let conversationExists: Bool
do {
- conversationExists = try await mlsService.conversationExists(groupID: mlsGroupID)
+ conversationExists = try await mlsService.conversationExists(
+ groupID: mlsGroupID
+ )
} catch {
conversationExists = false
}
@@ -108,5 +113,4 @@ extension ConversationLocalStore {
context.saveOrRollback()
}
}
-
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore.swift
index cbb1ca47fb4..3aaa338a9ba 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore.swift
@@ -17,8 +17,8 @@
//
import CoreData
-import WireAPI
import WireDataModel
+import WireLogging
// sourcery: AutoMockable
/// A local store dedicated to conversations.
@@ -49,26 +49,31 @@ public protocol ConversationLocalStoreProtocol {
/// - Parameter isFederationEnabled: A flag indicating whether a `Federation` is enabled.
func storeConversation(
- _ conversation: WireAPI.Conversation,
+ _ conversation: WireDomain.Conversation,
timestamp: Date,
- isFederationEnabled: Bool
+ isFederationEnabled: Bool,
+ isMLSEnabled: Bool
) async
/// Stores a flag indicating whether a conversation requires an update from backend.
/// - Parameter needsBackendUpdate: A flag indicated whether the qualified conversation needs to be updated from
/// backend.
- /// - Parameter qualifiedId: The conversation qualified ID.
+ /// - Parameter conversationID: The conversation ID.
+ /// - Parameter conversationDomain: The conversation domain.
func storeConversation(
needsBackendUpdate: Bool,
- qualifiedId: WireAPI.QualifiedID
+ conversationID: UUID,
+ conversationDomain: String
) async
/// Stores a given failed conversation locally.
- /// - Parameter qualifiedId: The conversation qualified ID.
+ /// - Parameter conversationID: The conversation ID.
+ /// - Parameter conversationDomain: The conversation domain.
func storeFailedConversation(
- withQualifiedId qualifiedId: WireAPI.QualifiedID
+ conversationID: UUID,
+ conversationDomain: String
) async
/// Fetches a MLS conversation locally.
@@ -103,13 +108,15 @@ public protocol ConversationLocalStoreProtocol {
/// Removes a given user from all group conversations.
///
/// - parameters:
- /// - user: The user to remove from the conversations.
+ /// - participantID: The ID of the participant to remove.
+ /// - participantDomain: The domain of the participant.
/// - date: The date the user was removed from the conversations.
func removeParticipantFromAllGroupConversations(
- user: ZMUser,
+ participantID: UUID,
+ participantDomain: String?,
date: Date
- ) async
+ ) async throws
/// Adds a participant or updates its role in a conversation.
///
@@ -132,13 +139,13 @@ public protocol ConversationLocalStoreProtocol {
/// - newParticipants: The id, domain and role of the new participant.
/// - sender: The user who added the participants.
/// - date: The date the participants were added.
- /// - conversation: The conversation to add the participants
+ /// - conversation: The conversation to add the participants to.
func addParticipants(
_ participants: [(id: UUID, domain: String?, role: String?)],
addedBy sender: (id: UUID, domain: String?),
atDate date: Date,
- to conversation: ZMConversation
+ conversation: (id: UUID, domain: String)
) async throws
/// Updates the member muted and archived status.
@@ -174,42 +181,6 @@ public protocol ConversationLocalStoreProtocol {
for conversation: ZMConversation
) async -> WireDataModel.MessageProtocol
- /// Adds a system message to a given conversation.
- /// - parameters:
- /// - message: The system message to add.
- /// - conversation: The conversation to add the system message to.
-
- func addSystemMessage(
- _ message: SystemMessage,
- to conversation: ZMConversation
- ) async
-
- /// Retrieves conversation muted message types
- /// - parameter conversation: The conversation to get the muted message types for.
- /// - returns: The muted message types.
-
- func conversationMutedMessageTypes(
- _ conversation: ZMConversation
- ) async -> MutedMessageTypes
-
- /// Stores a flag indicating whether a conversation is archived.
- /// - parameters:
- /// - isArchived: Indicates whether the conversation is archived.
- /// - conversation: The conversation to set the `isArchived` flag for.
-
- func storeConversation(
- isArchived: Bool,
- for conversation: ZMConversation
- ) async
-
- /// Indicates whether a conversation is archived.
- /// - parameter conversation: The conversation to check the `isArchived` flag for.
- /// - returns: A flag indicating whether the conversation is archived.
-
- func isConversationArchived(
- _ conversation: ZMConversation
- ) async -> Bool
-
/// Stores a flag indicating whether a conversation has read receipts enabled.
/// - parameters:
/// - hasReadReceiptsEnabled: A flag indicating whether the conversation has read receipts enabled.
@@ -232,6 +203,24 @@ public protocol ConversationLocalStoreProtocol {
initiatingUser: ZMUser
) async
+ /// The conversation active message destruction timeout value.
+ /// - Parameters:
+ /// - conversation: The conversation to get the message destruction timeout value from.
+ /// - returns: A `MessageDestructionTimeoutValue` object.
+
+ func conversationMessageDestructionTimeout(
+ _ conversation: ZMConversation
+ ) async -> MessageDestructionTimeoutValue
+
+ /// Stores a message destruction timeout value.
+ /// - parameters:
+ /// - timeoutValue: The message destruction timeout value.
+ /// - conversation: The conversation to update the value for.
+
+ func storeConversation(
+ timeoutValue: Double,
+ for conversation: ZMConversation
+ ) async
/// Fetches or creates a role locally.
/// - Parameters:
/// - role: The role name to fetch or create.
@@ -292,6 +281,72 @@ public protocol ConversationLocalStoreProtocol {
for conversation: ZMConversation
) async -> MLSGroupID?
+ /// Sends a notification using the main context informing typing users
+ /// have been updated for a given conversation.
+ /// - Parameters:
+ /// - conversationID: The conversation managed object ID.
+ /// - usersID: The updated typing users managed object IDs.
+
+ func updateTypingUsers(
+ conversationID: NSManagedObjectID,
+ usersID: Set
+ ) async
+
+ /// Obtain permanent stored object IDs.
+ /// - Parameters:
+ /// - user: The user to get the permanent managed object ID for.
+ /// - conversation: The conversation to get the permanent managed object ID for.
+
+ func obtainPermanentIDs(
+ user: ZMUser,
+ conversation: ZMConversation
+ )
+
+ /// Fetches the current conversation name
+ /// - parameter conversation: The conversation to fetch the name for.
+ /// - returns: The conversation name
+
+ func conversationName(
+ conversation: ZMConversation
+ ) async -> String?
+
+ /// Updates the conversation name.
+ /// - Parameters:
+ /// - newName: The new name for the conversation.
+ /// - conversation: The conversation to update the name for.
+
+ func storeConversation(
+ newName: String,
+ conversation: ZMConversation
+ ) async
+
+ /// Updates or creates a MLS group locally.
+ /// - Parameters:
+ /// - groupID: The MLS group ID.
+
+ func updateOrCreateMLSGroup(
+ groupID: MLSGroupID
+ ) async
+
+ /// Stores the conversation MLS group ID and marks the mls status as ready.
+ /// - Parameters:
+ /// - mlsGroupID: The MLS group ID related to the conversation.
+ /// - conversation: The conversation to update the properties for.
+
+ func storeMLSConversationEstablished(
+ mlsGroupID: MLSGroupID,
+ conversation: ZMConversation
+ ) async
+
+ /// Fetches the other user qualified id (not self user) in a 1:1 conversation.
+ /// - Parameters:
+ /// - conversation: The 1:1 conversation self and other user should be part of.
+ /// - returns: The other user `QualifiedID`.
+
+ func fetchOtherUserIDInOneOnOneConversation(
+ conversation: ZMConversation
+ ) async -> WireDataModel.QualifiedID?
+
}
public final class ConversationLocalStore: ConversationLocalStoreProtocol {
@@ -308,21 +363,79 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
let mlsLogger = WireLogger.mls
let updateEventLogger = WireLogger.updateEvent
let userLocalStore: any UserLocalStoreProtocol
+ let messageLocalStore: any MessageLocalStoreProtocol
// MARK: - Object lifecycle
public init(
context: NSManagedObjectContext,
mlsService: MLSServiceInterface,
- userLocalStore: any UserLocalStoreProtocol
+ userLocalStore: any UserLocalStoreProtocol,
+ messageLocalStore: any MessageLocalStoreProtocol
) {
self.context = context
self.mlsService = mlsService
self.userLocalStore = userLocalStore
+ self.messageLocalStore = messageLocalStore
}
// MARK: - Public
+ public func storeMLSConversationEstablished(
+ mlsGroupID: MLSGroupID,
+ conversation: ZMConversation
+ ) async {
+ await context.perform {
+ conversation.mlsStatus = .ready
+ conversation.mlsGroupID = mlsGroupID
+ }
+ }
+
+ public func updateOrCreateMLSGroup(
+ groupID: MLSGroupID
+ ) async {
+ await context.perform { [context] in
+
+ MLSGroup.updateOrCreate(
+ id: groupID,
+ inSyncContext: context
+ ) {
+ $0.lastKeyMaterialUpdate = .now
+ }
+ }
+ }
+
+ public func fetchOtherUserIDInOneOnOneConversation(
+ conversation: ZMConversation
+ ) async -> WireDataModel.QualifiedID? {
+ await context.perform {
+ guard conversation.conversationType == .oneOnOne else {
+ WireLogger.conversation.info(
+ "conversation type is not expected 'oneOnOne', aborting."
+ )
+
+ return nil
+ }
+
+ guard
+ let otherUser = conversation.localParticipantsExcludingSelf.first,
+ let otherUserID = otherUser.remoteIdentifier,
+ let otherUserDomain = otherUser.domain ?? BackendInfo.domain
+ else {
+ WireLogger.conversation.warn(
+ "failed to retrieve other user in 1:1 conversation"
+ )
+
+ return nil
+ }
+
+ return QualifiedID(
+ uuid: otherUserID,
+ domain: otherUserDomain
+ )
+ }
+ }
+
public func updateMemberStatus(
mutedStatusInfo: (status: Int?, referenceDate: Date?),
archivedStatusInfo: (status: Bool?, referenceDate: Date?),
@@ -398,10 +511,24 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
_ participants: [(id: UUID, domain: String?, role: String?)],
addedBy sender: (id: UUID, domain: String?),
atDate date: Date,
- to conversation: ZMConversation
+ conversation: (id: UUID, domain: String)
) async throws {
typealias UserAndRole = (user: ZMUser, role: Role?)
+ let localConversation = await fetchConversation(
+ id: conversation.id,
+ domain: conversation.domain
+ )
+
+ guard let localConversation else {
+ return WireLogger.eventProcessing.error(
+ "Member join update missing conversation, aborting... ",
+ attributes: [
+ .conversationId: conversation.id.safeForLoggingDescription
+ ]
+ )
+ }
+
let usersAndRoles = await withTaskGroup(of: UserAndRole?.self) { taskGroup in
for newParticipant in participants {
taskGroup.addTask { [self] in
@@ -413,7 +540,7 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
if let participantRole = newParticipant.role {
let role = await fetchOrCreateRole(
participantRole,
- in: conversation
+ in: localConversation
)
return (user, role)
@@ -434,34 +561,71 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
let users = Set(usersAndRoles.map(\.user))
let existingUsers = await localParticipants(
- in: conversation
+ in: localConversation
)
let newUsers = users.subtracting(existingUsers)
- if !newUsers.isEmpty, await isGroupConversation(conversation) {
- let sender = try await userLocalStore.fetchUser(
- id: sender.id,
- domain: sender.domain
- )
+ if !newUsers.isEmpty, await isGroupConversation(localConversation) {
- let systemMessage = SystemMessage(
- type: .participantsAdded,
+ let systemMessageType: MessageType = .participantsAdded(
+ participants: participants.map { ($0.id, $0.domain) },
sender: sender,
- users: newUsers,
- clients: nil,
- timestamp: date
+ date: date
)
- await addSystemMessage(systemMessage, to: conversation)
+ await messageLocalStore.addSystemMessageToConversation(
+ messageType: systemMessageType,
+ conversationID: conversation.id,
+ conversationDomain: conversation.domain
+ )
}
await context.perform {
- conversation.addParticipantsAndUpdateConversationState(
+ localConversation.addParticipantsAndUpdateConversationState(
usersAndRoles: usersAndRoles
)
}
}
+ public func updateTypingUsers(
+ conversationID: NSManagedObjectID,
+ usersID: Set
+ ) async {
+ await context.perform { [context] in
+ if let conversation = context.object(with: conversationID) as? ZMConversation {
+
+ let users = usersID.compactMap {
+ context.object(with: $0) as? ZMUser
+ }
+
+ context.typingUsers?.update(
+ typingUsers: Set(users),
+ in: conversation
+ )
+
+ self.notifyTypingUsers(
+ Set(users),
+ in: conversation
+ )
+ }
+ }
+ }
+
+ public func obtainPermanentIDs(
+ user: ZMUser,
+ conversation: ZMConversation
+ ) {
+ if user.objectID.isTemporaryID || conversation.objectID.isTemporaryID {
+ do {
+ try context.obtainPermanentIDs(for: [user, conversation])
+ } catch {
+ WireLogger.eventProcessing.error(
+ "Failed to obtain permanent object ids: \(error.localizedDescription)"
+ )
+ }
+ }
+ }
+
public func addSystemMessage(
_ message: SystemMessage,
to conversation: ZMConversation
@@ -492,9 +656,10 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
}
public func storeConversation(
- _ conversation: WireAPI.Conversation,
+ _ conversation: Conversation,
timestamp: Date,
- isFederationEnabled: Bool
+ isFederationEnabled: Bool,
+ isMLSEnabled: Bool
) async {
guard let conversationType = conversation.type else {
return
@@ -521,16 +686,17 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
switch conversationType {
case .group:
await updateOrCreateGroupConversation(
- remoteConversation: conversation,
- remoteConversationID: id,
+ from: conversation,
+ withID: id,
serverTimestamp: timestamp,
- isFederationEnabled: isFederationEnabled
+ isFederationEnabled: isFederationEnabled,
+ isMLSEnabled: isMLSEnabled
)
case .`self`:
await updateOrCreateSelfConversation(
- remoteConversation: conversation,
- remoteConversationID: id,
+ from: conversation,
+ withID: id,
serverTimestamp: timestamp,
isFederationEnabled: isFederationEnabled
)
@@ -539,8 +705,8 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
/// Conversations are of type `connection` while the connection
/// is pending.
await updateOrCreateConnectionConversation(
- remoteConversation: conversation,
- remoteConversationID: id,
+ from: conversation,
+ withID: id,
serverTimestamp: timestamp,
isFederationEnabled: isFederationEnabled
)
@@ -549,8 +715,8 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
/// Conversations are of type `oneOnOne` when the connection
/// is accepted.
await updateOrCreateOneToOneConversation(
- remoteConversation: conversation,
- remoteConversationID: id,
+ from: conversation,
+ withID: id,
serverTimestamp: timestamp,
isFederationEnabled: isFederationEnabled
)
@@ -559,12 +725,13 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
public func storeConversation(
needsBackendUpdate: Bool,
- qualifiedId: WireAPI.QualifiedID
+ conversationID: UUID,
+ conversationDomain: String
) async {
await context.perform { [context] in
let conversation = ZMConversation.fetch(
- with: qualifiedId.uuid,
- domain: qualifiedId.domain,
+ with: conversationID,
+ domain: conversationDomain,
in: context
)
@@ -573,11 +740,12 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
}
public func storeFailedConversation(
- withQualifiedId qualifiedId: WireAPI.QualifiedID
+ conversationID: UUID,
+ conversationDomain: String
) async {
let conversation = await fetchOrCreateConversation(
- id: qualifiedId.uuid,
- domain: qualifiedId.domain
+ id: conversationID,
+ domain: conversationDomain
)
await context.perform {
@@ -613,12 +781,23 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
}
}
+ public func conversationMessageDestructionTimeout(
+ _ conversation: ZMConversation
+ ) async -> MessageDestructionTimeoutValue {
+ await context.perform {
+ conversation.activeMessageDestructionTimeoutValue ?? .init(rawValue: 0)
+ }
+ }
+
public func storeConversation(
- isArchived: Bool,
+ timeoutValue: Double,
for conversation: ZMConversation
) async {
await context.perform {
- conversation.isArchived = isArchived
+ conversation.setMessageDestructionTimeoutValue(
+ .init(rawValue: timeoutValue),
+ for: .groupConversation
+ )
}
}
@@ -648,9 +827,16 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
}
public func removeParticipantFromAllGroupConversations(
- user: ZMUser,
+ participantID: UUID,
+ participantDomain: String?,
date: Date
- ) async {
+ ) async throws {
+
+ let user = try await userLocalStore.fetchUser(
+ id: participantID,
+ domain: participantDomain
+ )
+
let allGroupConversations = await context.perform {
// swiftformat:disable:next redundantProperty
let allGroupConversations: [ZMConversation] = user.participantRoles.compactMap {
@@ -665,33 +851,42 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
}
for conversation in allGroupConversations {
- let (userTeam, isTeamMember) = await context.perform {
- (user.team, user.isTeamMember)
+ let (userTeam, isTeamMember, conversationTeam, conversationID, conversationDomain) = await context.perform {
+ (
+ user.team,
+ user.isTeamMember,
+ conversation.team,
+ conversation.remoteIdentifier as UUID,
+ conversation.domain
+ )
}
- let teamConversation = await context.perform {
- conversation.team
- }
+ if isTeamMember, conversationTeam == userTeam {
- if isTeamMember, teamConversation == userTeam {
- let systemMessage = SystemMessage(
- type: .teamMemberLeave,
- sender: user,
- users: [user],
- timestamp: date
+ let systemMessageType: MessageType = .teamMemberRemoved(
+ member: (participantID, participantDomain),
+ date: date
)
- await addSystemMessage(systemMessage, to: conversation)
+ await messageLocalStore.addSystemMessageToConversation(
+ messageType: systemMessageType,
+ conversationID: conversationID,
+ conversationDomain: conversationDomain
+ )
} else {
- let systemMessage = SystemMessage(
- type: .participantsRemoved,
- sender: user,
- users: [user],
- timestamp: date
+
+ let systemMessageType: MessageType = .participantsRemoved(
+ participants: [(participantID, participantDomain)],
+ sender: (participantID, participantDomain),
+ date: date
)
- await addSystemMessage(systemMessage, to: conversation)
+ await messageLocalStore.addSystemMessageToConversation(
+ messageType: systemMessageType,
+ conversationID: conversationID,
+ conversationDomain: conversationDomain
+ )
}
await context.perform {
@@ -786,52 +981,82 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
}
}
+ public func conversationName(
+ conversation: ZMConversation
+ ) async -> String? {
+ await context.perform {
+ conversation.userDefinedName
+ }
+ }
+
+ public func storeConversation(
+ newName: String,
+ conversation: ZMConversation
+ ) async {
+ await context.perform {
+ conversation.userDefinedName = newName
+ }
+ }
+
// MARK: - Private
+ private func notifyTypingUsers(
+ _ typingUsers: Set,
+ in conversation: ZMConversation
+ ) {
+ let typingNotificationUsersKey = "typingUsers"
+
+ NotificationInContext(
+ name: .typingNotification,
+ context: context.notificationContext,
+ object: self,
+ userInfo: [typingNotificationUsersKey: typingUsers]
+ ).post()
+ }
+
/// Updates or creates a conversation of type `connection` locally.
///
/// See and for more information.
///
- /// - Parameter remoteConversation: The conversation object received from backend.
- /// - Parameter removeConversationID: The conversation ID received from backend.
+ /// - Parameter conversation: The up-to-date conversation object.
+ /// - Parameter id: The conversation ID.
/// - Parameter isFederationEnabled: A flag indicating whether a federation is enabled.
private func updateOrCreateConnectionConversation(
- remoteConversation: WireAPI.Conversation,
- remoteConversationID: UUID,
+ from conversation: Conversation,
+ withID id: UUID,
serverTimestamp: Date,
isFederationEnabled: Bool
) async {
- let conversation = await fetchOrCreateConversation(
- id: remoteConversationID,
- domain: remoteConversation.qualifiedID?.domain
+ let localConversation = await fetchOrCreateConversation(
+ id: id,
+ domain: conversation.qualifiedID?.domain
)
await context.perform { [self] in
- conversation.conversationType = .connection
+ localConversation.conversationType = .connection
commonUpdate(
- from: remoteConversation,
- for: conversation,
+ from: conversation,
+ for: localConversation,
serverTimestamp: serverTimestamp,
isFederationEnabled: isFederationEnabled
)
-
assignMessageProtocol(
- from: remoteConversation,
- for: conversation
+ from: conversation,
+ for: localConversation
)
updateConversationStatus(
- from: remoteConversation,
- for: conversation
+ from: conversation,
+ for: localConversation
)
- conversation.needsToBeUpdatedFromBackend = false
- conversation.isPendingInitialFetch = false
+ localConversation.needsToBeUpdatedFromBackend = false
+ localConversation.isPendingInitialFetch = false
}
- guard let selfMember = remoteConversation.members?.selfMember else {
+ guard let selfMember = conversation.members?.selfMember else {
return
}
@@ -841,7 +1066,7 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
await updateMemberStatus(
mutedStatusInfo: mutedStatusInfo,
archivedStatusInfo: archivedStatusInfo,
- for: conversation
+ for: localConversation
)
}
@@ -849,19 +1074,20 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
///
/// See and for more information.
///
- /// - Parameter remoteConversation: The conversation object received from backend.
- /// - Parameter removeConversationID: The conversation ID received from backend.
+ /// - Parameter conversation: The up-to-date conversation object.
+ /// - Parameter id: The conversation ID.
/// - Parameter isFederationEnabled: A flag indicating whether a federation is enabled.
private func updateOrCreateSelfConversation(
- remoteConversation: WireAPI.Conversation,
- remoteConversationID: UUID,
+ from conversation: Conversation,
+ withID id: UUID,
serverTimestamp: Date,
isFederationEnabled: Bool
) async {
- let conversation = await fetchOrCreateConversation(
- id: remoteConversationID,
- domain: remoteConversation.qualifiedID?.domain
+
+ let localConversation = await fetchOrCreateConversation(
+ id: id,
+ domain: conversation.qualifiedID?.domain
)
let mlsGroupID = await context.perform {
@@ -869,28 +1095,28 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
}
await context.perform { [self] in
- conversation.conversationType = .`self`
- conversation.isPendingMetadataRefresh = false
+ localConversation.conversationType = .`self`
+ localConversation.isPendingMetadataRefresh = false
commonUpdate(
- from: remoteConversation,
- for: conversation,
+ from: conversation,
+ for: localConversation,
serverTimestamp: serverTimestamp,
isFederationEnabled: isFederationEnabled
)
updateMessageProtocol(
- from: remoteConversation,
- for: conversation
+ from: conversation,
+ for: localConversation
)
- conversation.isPendingInitialFetch = false
- conversation.needsToBeUpdatedFromBackend = false
+ localConversation.isPendingInitialFetch = false
+ localConversation.needsToBeUpdatedFromBackend = false
}
if mlsGroupID != nil {
do {
- try await createOrJoinSelfConversation(from: conversation)
+ try await createOrJoinSelfConversation(from: localConversation)
} catch {
mlsLogger.error(
"createOrJoinSelfConversation threw error: \(String(reflecting: error))"
@@ -903,83 +1129,88 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
///
/// See and for more information.
///
- /// - Parameter remoteConversation: The conversation object received from backend.
- /// - Parameter removeConversationID: The conversation ID received from backend.
+ /// - Parameter conversation: The up-to-date conversation object.
+ /// - Parameter id: The conversation ID.
/// - Parameter isFederationEnabled: A flag indicating whether a federation is enabled.
private func updateOrCreateGroupConversation(
- remoteConversation: WireAPI.Conversation,
- remoteConversationID: UUID,
+ from conversation: Conversation,
+ withID id: UUID,
serverTimestamp: Date,
- isFederationEnabled: Bool
+ isFederationEnabled: Bool,
+ isMLSEnabled: Bool
) async {
var isInitialFetch = false
- let conversation = await fetchOrCreateConversation(
- id: remoteConversationID,
- domain: remoteConversation.qualifiedID?.domain
+ let localConversation = await fetchOrCreateConversation(
+ id: id,
+ domain: conversation.qualifiedID?.domain
)
await context.perform { [self] in
- isInitialFetch = conversation.isPendingInitialFetch
+ isInitialFetch = localConversation.isPendingInitialFetch
- conversation.conversationType = .group
- conversation.remoteIdentifier = remoteConversationID
- conversation.isPendingMetadataRefresh = false
- conversation.isPendingInitialFetch = false
+ localConversation.conversationType = .group
+ localConversation.remoteIdentifier = id
+ localConversation.isPendingMetadataRefresh = false
+ localConversation.isPendingInitialFetch = false
commonUpdate(
- from: remoteConversation,
- for: conversation,
+ from: conversation,
+ for: localConversation,
serverTimestamp: serverTimestamp,
isFederationEnabled: isFederationEnabled
)
updateConversationStatus(
- from: remoteConversation,
- for: conversation
+ from: conversation,
+ for: localConversation
)
if isInitialFetch {
assignMessageProtocol(
- from: remoteConversation,
- for: conversation
+ from: conversation,
+ for: localConversation
)
} else {
updateMessageProtocol(
- from: remoteConversation,
- for: conversation
+ from: conversation,
+ for: localConversation
)
}
Flow.createGroup.checkpoint(
- description: "conversation created remote id: \(conversation.remoteIdentifier?.safeForLoggingDescription ?? "")"
+ description: "conversation created remote id: \(localConversation.remoteIdentifier?.safeForLoggingDescription ?? "")"
)
}
- if let selfMember = remoteConversation.members?.selfMember {
+ if let selfMember = conversation.members?.selfMember {
let mutedStatusInfo = (selfMember.mutedStatus, selfMember.mutedReference)
let archivedStatusInfo = (selfMember.archived, selfMember.archivedReference)
await updateMemberStatus(
mutedStatusInfo: mutedStatusInfo,
archivedStatusInfo: archivedStatusInfo,
- for: conversation
+ for: localConversation
)
}
- await updateMLSStatus(from: remoteConversation, for: conversation)
+ await updateMLSStatus(
+ from: conversation,
+ for: localConversation,
+ isMLSEnabled: isMLSEnabled
+ )
await context.perform { [self] in
if isInitialFetch {
/// we just got a new conversation, we display new conversation header
- conversation.appendNewConversationSystemMessage(
+ localConversation.appendNewConversationSystemMessage(
at: .distantPast,
- users: conversation.localParticipants
+ users: localConversation.localParticipants
)
/// Slow synced conversations should be considered read from the start
- conversation.lastReadServerTimeStamp = conversation.lastModifiedDate
+ localConversation.lastReadServerTimeStamp = localConversation.lastModifiedDate
Flow.createGroup.checkpoint(
description: "new system message for conversation inserted"
@@ -988,7 +1219,7 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
/// If we discover this group is actually a fake one on one,
/// then we should link the one on one user.
- linkOneOnOneUserIfNeeded(for: conversation)
+ linkOneOnOneUserIfNeeded(for: localConversation)
}
}
@@ -996,23 +1227,23 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
///
/// See and for more information.
///
- /// - Parameter remoteConversation: The conversation object received from backend.
- /// - Parameter removeConversationID: The conversation ID received from backend.
+ /// - Parameter conversation: The up-to-date conversation object.
+ /// - Parameter id: The conversation ID.
/// - Parameter isFederationEnabled: A flag indicating whether a federation is enabled.
private func updateOrCreateOneToOneConversation(
- remoteConversation: WireAPI.Conversation,
- remoteConversationID: UUID,
+ from conversation: Conversation,
+ withID id: UUID,
serverTimestamp: Date,
isFederationEnabled: Bool
) async {
- guard let conversationTypeRawValue = remoteConversation.type?.rawValue else {
+ guard let conversationTypeRawValue = conversation.type?.rawValue else {
return
}
- let conversation = await fetchOrCreateConversation(
- id: remoteConversationID,
- domain: remoteConversation.qualifiedID?.domain
+ let localConversation = await fetchOrCreateConversation(
+ id: id,
+ domain: conversation.qualifiedID?.domain
)
await context.perform { [self] in
@@ -1020,40 +1251,40 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
rawValue: conversationTypeRawValue
)
- if conversation.oneOnOneUser?.connection?.status == .sent {
- conversation.conversationType = .connection
+ if localConversation.oneOnOneUser?.connection?.status == .sent {
+ localConversation.conversationType = .connection
} else {
- conversation.conversationType = conversationType
+ localConversation.conversationType = conversationType
}
assignMessageProtocol(
- from: remoteConversation,
- for: conversation
+ from: conversation,
+ for: localConversation
)
commonUpdate(
- from: remoteConversation,
- for: conversation,
+ from: conversation,
+ for: localConversation,
serverTimestamp: serverTimestamp,
isFederationEnabled: isFederationEnabled
)
- linkOneOnOneUserIfNeeded(for: conversation)
+ linkOneOnOneUserIfNeeded(for: localConversation)
- conversation.needsToBeUpdatedFromBackend = false
- conversation.isPendingInitialFetch = false
+ localConversation.needsToBeUpdatedFromBackend = false
+ localConversation.isPendingInitialFetch = false
updateConversationStatus(
- from: remoteConversation,
- for: conversation
+ from: conversation,
+ for: localConversation
)
- if let otherUser = conversation.localParticipantsExcludingSelf.first {
- conversation.isPendingMetadataRefresh = otherUser.isPendingMetadataRefresh
+ if let otherUser = localConversation.localParticipantsExcludingSelf.first {
+ localConversation.isPendingMetadataRefresh = otherUser.isPendingMetadataRefresh
}
}
- guard let selfMember = remoteConversation.members?.selfMember else {
+ guard let selfMember = conversation.members?.selfMember else {
return
}
@@ -1063,36 +1294,36 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
await updateMemberStatus(
mutedStatusInfo: mutedStatusInfo,
archivedStatusInfo: archivedStatusInfo,
- for: conversation
+ for: localConversation
)
}
/// A common update method for all conversations received, no matter the type of the conversation.
///
- /// - Parameter remoteConversation: The conversation object received from backend.
+ /// - Parameter conversation: The up-to-date conversation object.
/// - Parameter localConversation: The local conversation to update.
/// - Parameter isFederationEnabled: A flag indicating whether a federation is enabled.
/// - Parameter serverTimestamp: The date the conversation was created/updated.
private func commonUpdate(
- from remoteConversation: WireAPI.Conversation,
+ from conversation: Conversation,
for localConversation: ZMConversation,
serverTimestamp: Date,
isFederationEnabled: Bool
) {
updateAttributes(
- from: remoteConversation,
+ from: conversation,
for: localConversation,
isFederationEnabled: isFederationEnabled
)
updateMetadata(
- from: remoteConversation,
+ from: conversation,
for: localConversation
)
updateMembers(
- from: remoteConversation,
+ from: conversation,
for: localConversation
)
@@ -1106,7 +1337,7 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
/// block.
///
/// - Parameter conversationID: The conversation ID to fetch or create the local conversation from.
- /// - Parameter domain: The domain to fetch or create the conversation from.
+ /// - Parameter conversationDomain: The domain to fetch or create the conversation from.
/// - Parameter handler: A completion block that takes a `ZMConversation` as argument and returns
/// a `ZMConversation` and an optional `MLSGroupID`.
///
@@ -1116,12 +1347,13 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
@discardableResult
private func fetchOrCreateConversation(
conversationID: UUID,
- domain: String?,
+ conversationDomain: String?,
handler: @escaping (ZMConversation) -> (ZMConversation, MLSGroupID?)
) async -> (ZMConversation, MLSGroupID?) {
+
let conversation = await fetchOrCreateConversation(
id: conversationID,
- domain: domain
+ domain: conversationDomain
)
return await context.perform {
diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationModelMappings.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationModelMappings.swift
index 0fcf229f41b..b58b57de33f 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationModelMappings.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationModelMappings.swift
@@ -76,10 +76,87 @@ extension WireAPI.ConversationMemberLeaveReason {
func toDomainModel() -> ZMSystemMessageType {
switch self {
- case .userDeleted, .left:
+ case .userDeleted, .userLeft:
.teamMemberLeave
- case .removed:
+ case .userRemoved:
.participantsRemoved
}
}
}
+
+extension WireAPI.ConversationType {
+
+ func toDomainModel() -> WireDataModel.BackendConversationType {
+ switch self {
+ case .group:
+ .group
+ case .self:
+ .`self`
+ case .oneOnOne:
+ .oneOnOne
+ case .connection:
+ .connection
+ }
+ }
+
+}
+
+extension WireAPI.Conversation.Members {
+
+ func toDomainModel() -> WireDomain.Conversation.Members {
+ .init(
+ others: others.map { $0.toDomainModel() },
+ selfMember: selfMember.toDomainModel()
+ )
+ }
+
+}
+
+extension WireAPI.Conversation.Member {
+
+ func toDomainModel() -> WireDomain.Conversation.Members.Member {
+ .init(
+ qualifiedID: qualifiedID?.toDomainModel(),
+ id: id,
+ qualifiedTarget: qualifiedTarget?.toDomainModel(),
+ target: target,
+ conversationRole: conversationRole,
+ service: (service != nil) ? (service!.id, service!.provider) : nil,
+ archived: archived,
+ archivedReference: archivedReference,
+ hidden: hidden,
+ hiddenReference: hiddenReference,
+ mutedStatus: mutedStatus,
+ mutedReference: mutedReference
+ )
+ }
+
+}
+
+extension WireAPI.Conversation {
+
+ func toDomainModel() -> WireDomain.Conversation {
+ .init(
+ id: id,
+ qualifiedID: qualifiedID?.toDomainModel(),
+ teamID: teamID,
+ type: type?.toDomainModel(),
+ messageProtocol: messageProtocol?.toDomainModel(),
+ mlsGroupID: mlsGroupID,
+ cipherSuite: cipherSuite?.toDomainModel(),
+ epoch: epoch,
+ epochTimestamp: epochTimestamp,
+ creator: creator,
+ members: members?.toDomainModel(),
+ name: name,
+ messageTimer: messageTimer,
+ readReceiptMode: readReceiptMode,
+ access: access?.map(\.rawValue),
+ accessRoles: accessRoles?.map(\.rawValue),
+ legacyAccessRole: legacyAccessRole?.toDomainModel(),
+ lastEvent: lastEvent,
+ lastEventTime: lastEventTime
+ )
+ }
+
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepository.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepository.swift
index c60e61fc6cb..0762dcb0056 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepository.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepository.swift
@@ -19,6 +19,7 @@
import Foundation
import WireAPI
import WireDataModel
+import WireLogging
// sourcery: AutoMockable
/// Facilitate access to conversations related domain objects.
@@ -51,7 +52,7 @@ public protocol ConversationRepositoryProtocol {
/// - timestamp: The date the conversation was created or last modified.
func storeConversation(
- _ conversation: WireAPI.Conversation,
+ _ conversation: WireDomain.Conversation,
timestamp: Date
) async
@@ -170,15 +171,40 @@ public protocol ConversationRepositoryProtocol {
reason: ConversationMemberLeaveReason
) async throws
- /// Adds a system message to a given conversation.
- /// - parameters:
- /// - message: The system message to add.
- /// - conversation: The conversation to add the system message to.
+ /// Updates the conversation name locally.
+ /// - Parameters:
+ /// - newName: The new name for the conversation.
+ /// - conversationID: The conversation ID.
+ /// - conversationDomain: The conversation domain.
+ /// - senderID: The user ID.
+ /// - senderDomain: The user domain.
+ /// - date: The date the conversation name was updated.
- func addSystemMessage(
- _ message: SystemMessage,
- to conversation: ZMConversation
+ func updateConversationName(
+ newName: String,
+ conversationID: UUID,
+ conversationDomain: String?,
+ senderID: UUID,
+ senderDomain: String?,
+ date: Date
) async
+
+ /// Updates the typing users for a given conversation.
+ /// - Parameters:
+ /// - typingUsersInfo: A list of typing users for a given conversation.
+
+ func updateTypingUsers(
+ _ typingUsersInfo: [ConversationTypingUsersInfo]
+ ) async
+
+ /// Fetches the guest link for a given conversation.
+ /// - parameter conversationID: The conversation id.
+ /// - returns: The guest link.
+
+ func fetchConversationGuestLink(
+ conversationID: String
+ ) async throws -> String?
+
}
public final class ConversationRepository: ConversationRepositoryProtocol {
@@ -186,6 +212,7 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
public struct BackendInfo {
let domain: String
let isFederationEnabled: Bool
+ let isMLSEnabled: Bool
}
// MARK: - Properties
@@ -194,6 +221,7 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
private let conversationsLocalStore: any ConversationLocalStoreProtocol
private let userRepository: any UserRepositoryProtocol
private let teamRepository: any TeamRepositoryProtocol
+ private let messageRepository: any MessageRepositoryProtocol
private let backendInfo: BackendInfo
private let mlsProvider: MLSProvider
@@ -204,6 +232,7 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
conversationsLocalStore: any ConversationLocalStoreProtocol,
userRepository: any UserRepositoryProtocol,
teamRepository: any TeamRepositoryProtocol,
+ messageRepository: any MessageRepositoryProtocol,
backendInfo: BackendInfo,
mlsProvider: MLSProvider
) {
@@ -211,12 +240,28 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
self.conversationsLocalStore = conversationsLocalStore
self.userRepository = userRepository
self.teamRepository = teamRepository
+ self.messageRepository = messageRepository
self.backendInfo = backendInfo
self.mlsProvider = mlsProvider
}
// MARK: - Public
+ public func fetchConversationGuestLink(
+ conversationID: String
+ ) async throws -> String? {
+
+ do {
+ return try await conversationsAPI.getConversationGuestLink(
+ conversationID: conversationID
+ )
+
+ } catch {
+ throw ConversationRepositoryError.failedToFetchGuestLink(error)
+ }
+
+ }
+
public func pullConversation(id: UUID, domain: String) async throws {
let qualifiedID = WireAPI.QualifiedID(uuid: id, domain: domain)
let conversationList = try await conversationsAPI.getConversations(
@@ -228,9 +273,10 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
}
await conversationsLocalStore.storeConversation(
- conversation,
+ conversation.toDomainModel(),
timestamp: .now,
- isFederationEnabled: backendInfo.isFederationEnabled
+ isFederationEnabled: backendInfo.isFederationEnabled,
+ isMLSEnabled: backendInfo.isMLSEnabled
)
}
@@ -255,25 +301,29 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
}
public func storeConversation(
- _ conversation: WireAPI.Conversation,
+ _ conversation: Conversation,
timestamp: Date
) async {
await conversationsLocalStore.storeConversation(
conversation,
timestamp: timestamp,
- isFederationEnabled: backendInfo.isFederationEnabled
+ isFederationEnabled: backendInfo.isFederationEnabled,
+ isMLSEnabled: backendInfo.isMLSEnabled
)
}
public func pullConversations() async throws {
var qualifiedIds: [WireAPI.QualifiedID]
- if let result = try? await conversationsAPI
- .getLegacyConversationIdentifiers() { // only for api v0 (see `ConversationsAPIV0` method comment)
+ if let result =
+ try? await conversationsAPI
+ .getLegacyConversationIdentifiers() { // only for api v0 (see `ConversationsAPIV0` method comment)
let uuids = try await result.reduce(into: [UUID]()) { partialResult, uuids in
partialResult.append(contentsOf: uuids)
}
- qualifiedIds = uuids.map { WireAPI.QualifiedID(uuid: $0, domain: backendInfo.domain) }
+ qualifiedIds = uuids.map {
+ WireAPI.QualifiedID(uuid: $0, domain: backendInfo.domain)
+ }
} else {
// fallback to api versions > v0.
let ids = try await conversationsAPI.getConversationIdentifiers()
@@ -282,7 +332,9 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
}
}
- let conversationList = try await conversationsAPI.getConversations(for: qualifiedIds)
+ let conversationList = try await conversationsAPI.getConversations(
+ for: qualifiedIds
+ )
await withThrowingTaskGroup(of: Void.self) { taskGroup in
let foundConversations = conversationList.found
@@ -292,7 +344,7 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
for conversation in foundConversations {
taskGroup.addTask { [self] in
await storeConversation(
- conversation,
+ conversation.toDomainModel(),
timestamp: .now
)
}
@@ -302,7 +354,8 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
taskGroup.addTask { [self] in
await conversationsLocalStore.storeConversation(
needsBackendUpdate: true,
- qualifiedId: id
+ conversationID: id.uuid,
+ conversationDomain: id.domain
)
}
}
@@ -310,7 +363,8 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
for id in failedConversationsQualifiedIds {
taskGroup.addTask { [self] in
await conversationsLocalStore.storeFailedConversation(
- withQualifiedId: id
+ conversationID: id.uuid,
+ conversationDomain: id.domain
)
}
}
@@ -321,19 +375,21 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
userID: String,
userDomain: String
) async throws -> String {
- let mlsConversation = try await conversationsAPI.getMLSOneToOneConversation(
- userID: userID,
- in: userDomain
- )
+ let mlsConversation =
+ try await conversationsAPI.getMLSOneToOneConversation(
+ userID: userID,
+ in: userDomain
+ )
guard let mlsGroupID = mlsConversation.mlsGroupID else {
throw ConversationRepositoryError.mlsConversationShouldHaveAGroupID
}
await conversationsLocalStore.storeConversation(
- mlsConversation,
+ mlsConversation.toDomainModel(),
timestamp: .now,
- isFederationEnabled: backendInfo.isFederationEnabled
+ isFederationEnabled: backendInfo.isFederationEnabled,
+ isMLSEnabled: backendInfo.isMLSEnabled
)
return mlsGroupID
@@ -356,25 +412,61 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
participantDomain: String?,
removedAt date: Date
) async throws {
- let user = try await userRepository.fetchUser(
- id: participantID,
- domain: participantDomain
- )
- await conversationsLocalStore.removeParticipantFromAllGroupConversations(
- user: user,
+ try await conversationsLocalStore.removeParticipantFromAllGroupConversations(
+ participantID: participantID,
+ participantDomain: participantDomain,
date: date
)
}
+ public func updateConversationName(
+ newName: String,
+ conversationID: UUID,
+ conversationDomain: String?,
+ senderID: UUID,
+ senderDomain: String?,
+ date: Date
+ ) async {
+
+ let conversation = await fetchOrCreateConversation(
+ id: conversationID,
+ domain: conversationDomain
+ )
+
+ let currentConversationName = await conversationsLocalStore.conversationName(conversation: conversation)
+
+ if currentConversationName != newName {
+ let messageType = MessageType.conversationNameChanged(
+ newName: newName,
+ sender: (senderID, senderDomain),
+ date: date
+ )
+
+ await messageRepository.addMessageToConversation(
+ messageType: messageType,
+ conversationID: conversationID,
+ conversationDomain: conversationDomain
+ )
+ }
+
+ await conversationsLocalStore.storeConversation(
+ newName: newName,
+ conversation: conversation
+ )
+
+ }
+
public func deleteConversation(
id: UUID,
domain: String?
) async throws {
- guard let conversation = await fetchConversation(
- id: id,
- domain: domain
- ) else {
+ guard
+ let conversation = await fetchConversation(
+ id: id,
+ domain: domain
+ )
+ else {
return WireLogger.conversation.warn(
"Cannot delete a conversation that doesn't exist locally: \(id.safeForLoggingDescription)"
)
@@ -390,7 +482,9 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
)
if let mlsGroupID {
- try await conversationsLocalStore.wipeMLSGroup(groupID: mlsGroupID)
+ try await conversationsLocalStore.wipeMLSGroup(
+ groupID: mlsGroupID
+ )
}
await conversationsLocalStore.deleteConversation(
@@ -435,7 +529,7 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
conversationID: UUID,
conversationDomain: String
) async throws {
- var conversation = await fetchConversation(
+ let conversation = await fetchConversation(
id: conversationID,
domain: conversationDomain
)
@@ -446,27 +540,13 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
id: conversationID,
domain: conversationDomain
)
-
- conversation = await fetchConversation(
- id: conversationID,
- domain: conversationDomain
- )
- }
-
- guard let conversation else {
- return WireLogger.eventProcessing.error(
- "Member join update missing conversation, aborting... ",
- attributes: [
- .conversationId: conversationID.safeForLoggingDescription
- ]
- )
}
try await conversationsLocalStore.addParticipants(
participants,
addedBy: sender,
atDate: date,
- to: conversation
+ conversation: (conversationID, conversationDomain)
)
}
@@ -474,45 +554,55 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
_ userIDs: Set,
from conversation: ConversationID,
initiatedBy sender: UserID,
- at time: Date,
+ at date: Date,
reason: ConversationMemberLeaveReason
) async throws {
- let id = conversation.uuid
- let domain = conversation.domain
+ let conversationID = conversation.uuid
+ let conversationDomain = conversation.domain
+ let senderID = sender.uuid
+ let senderDomain = sender.domain
let removedUserIDs = userIDs
- let conversation = await conversationsLocalStore.fetchOrCreateConversation(
- id: id,
- domain: domain
- )
+ let conversation =
+ await conversationsLocalStore.fetchOrCreateConversation(
+ id: conversationID,
+ domain: conversationDomain
+ )
let removedUsers = await getRemovedUsers(from: removedUserIDs)
- let participants = await conversationsLocalStore.localParticipants(in: conversation)
+ let participants = await conversationsLocalStore.localParticipants(
+ in: conversation
+ )
let sender = try await userRepository.fetchUser(
- id: sender.uuid,
- domain: sender.domain
+ id: senderID,
+ domain: senderDomain
)
if !participants.isDisjoint(with: removedUsers) {
- let systemMessage = SystemMessage(
- type: reason.toDomainModel(),
- sender: sender,
- timestamp: time
+ await addSystemMessage(
+ conversationID: conversationID,
+ conversationDomain: conversationDomain,
+ senderID: senderID,
+ senderDomain: senderDomain,
+ date: date,
+ removedUsers: removedUserIDs,
+ reason: reason
)
-
- await addSystemMessage(systemMessage, to: conversation)
}
let isSelfUserRemoved = await isSelfUserRemoved(in: removedUserIDs)
- let messageProtocol = await conversationsLocalStore.messageProtocol(for: conversation)
-
- await conversationsLocalStore.removeParticipantsAndUpdateConversationState(
- conversation: conversation,
- users: Set(removedUsers),
- initiatingUser: sender
+ let messageProtocol = await conversationsLocalStore.messageProtocol(
+ for: conversation
)
+ await conversationsLocalStore
+ .removeParticipantsAndUpdateConversationState(
+ conversation: conversation,
+ users: Set(removedUsers),
+ initiatingUser: sender
+ )
+
let isMLSEnabled = mlsProvider.isMLSEnabled
let mlsService = mlsProvider.service
@@ -521,7 +611,8 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
for: conversation
)
- if isSelfUserRemoved, let mlsGroupID, messageProtocol.isOne(of: .mls, .mixed) {
+ if isSelfUserRemoved, let mlsGroupID,
+ messageProtocol.isOne(of: .mls, .mixed) {
try await mlsService.wipeGroup(mlsGroupID)
}
}
@@ -530,21 +621,52 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
return
}
- await deleteMembership(for: removedUserIDs, time: time)
+ await deleteMembership(for: removedUserIDs, time: date)
}
- public func addSystemMessage(
- _ message: SystemMessage,
- to conversation: ZMConversation
+ public func updateTypingUsers(
+ _ typingUsersInfo: [ConversationTypingUsersInfo]
) async {
- await conversationsLocalStore.addSystemMessage(
- message,
- to: conversation
- )
+ for typingUserInfo in typingUsersInfo {
+ await conversationsLocalStore.updateTypingUsers(
+ conversationID: typingUserInfo.conversationID,
+ usersID: typingUserInfo.users
+ )
+ }
}
// MARK: - Private
+ private func addSystemMessage(
+ conversationID: UUID,
+ conversationDomain: String?,
+ senderID: UUID,
+ senderDomain: String?,
+ date: Date,
+ removedUsers: Set,
+ reason: ConversationMemberLeaveReason
+ ) async {
+ var systemMessageType: MessageType = switch reason {
+ case .userDeleted, .userLeft:
+ .teamMemberRemoved(
+ member: (senderID, senderDomain),
+ date: date
+ )
+ case .userRemoved:
+ .participantsRemoved(
+ participants: removedUsers.map { ($0.uuid, $0.domain) },
+ sender: (senderID, senderDomain),
+ date: date
+ )
+ }
+
+ await messageRepository.addMessageToConversation(
+ messageType: systemMessageType,
+ conversationID: conversationID,
+ conversationDomain: conversationDomain
+ )
+ }
+
private func getRemovedUsers(
from userIDs: Set
) async -> [WireDataModel.ZMUser] {
@@ -599,9 +721,9 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
taskGroup.addTask { [self] in
do {
try await teamRepository.deleteMembership(
- for: userID.uuid,
+ userID: userID.uuid,
domain: userID.domain,
- at: time
+ date: time
)
} catch {
WireLogger.eventProcessing.error(
diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepositoryError.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepositoryError.swift
index 2b5bc22653e..a38d05cc9e0 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepositoryError.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepositoryError.swift
@@ -34,4 +34,8 @@ enum ConversationRepositoryError: Error {
case mlsConversationShouldHaveAGroupID
+ /// Unable to fetch conversation guest link
+
+ case failedToFetchGuestLink(Error)
+
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/Models/Conversation.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/Models/Conversation.swift
new file mode 100644
index 00000000000..c14654eb77d
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/Models/Conversation.swift
@@ -0,0 +1,63 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+
+public struct Conversation {
+
+ struct Members {
+ let others: [Member]
+ let selfMember: Member
+
+ struct Member {
+ let qualifiedID: QualifiedID?
+ let id: UUID?
+ let qualifiedTarget: QualifiedID?
+ let target: UUID?
+ let conversationRole: String?
+ let service: (id: UUID, provider: UUID)?
+ let archived: Bool?
+ let archivedReference: Date?
+ let hidden: Bool?
+ let hiddenReference: String?
+ let mutedStatus: Int?
+ let mutedReference: Date?
+ }
+ }
+
+ let id: UUID?
+ let qualifiedID: QualifiedID?
+ let teamID: UUID?
+ let type: BackendConversationType?
+ let messageProtocol: MessageProtocol?
+ let mlsGroupID: String?
+ let cipherSuite: MLSCipherSuite?
+ let epoch: UInt?
+ let epochTimestamp: Date?
+ let creator: UUID?
+ let members: Members?
+ let name: String?
+ let messageTimer: TimeInterval?
+ let readReceiptMode: Int?
+ let access: [String]?
+ let accessRoles: [String]?
+ let legacyAccessRole: ConversationAccessRole?
+ let lastEvent: String?
+ let lastEventTime: Date?
+
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsLocalStore.swift
new file mode 100644
index 00000000000..4ea209ff051
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsLocalStore.swift
@@ -0,0 +1,160 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+import WireLogging
+
+// sourcery: AutoMockable
+public protocol ConversationLabelsLocalStoreProtocol {
+
+ /// Save label and related conversations objects to local storage.
+ /// - Parameter conversationLabel: conversation label info
+
+ func storeLabel(
+ _ conversationLabel: ConversationLabelInfo
+ ) async throws
+
+ /// Delete old `folder` labels and related conversations objects from local storage.
+ /// - Parameter excludedLabels: remote labels that should be excluded from deletion.
+ /// - Only old labels of type `folder` are deleted, `favorite` labels always remain in the local storage.
+
+ func deleteOldLabelsLocally(
+ excludedLabels: [ConversationLabelInfo]
+ ) async throws
+}
+
+public final class ConversationLabelsLocalStore: ConversationLabelsLocalStoreProtocol {
+
+ // MARK: - Error
+
+ enum Failure: Error {
+ case failedToStoreLabelLocally(UUID)
+ }
+
+ // MARK: - Properties
+
+ private let context: NSManagedObjectContext
+ private let logger = WireLogger(tag: "conversation-labels")
+
+ // MARK: - Object lifecycle
+
+ init(
+ context: NSManagedObjectContext
+ ) {
+ self.context = context
+ }
+
+ // MARK: - Public
+
+ /// Save label and related conversations objects to local storage.
+ /// - Parameter conversationLabel: conversation label
+
+ public func storeLabel(
+ _ conversationLabel: ConversationLabelInfo
+ ) async throws {
+ try await context.perform { [context] in
+ var created = false
+ let label: Label? = if conversationLabel.type == Label.Kind.favorite.rawValue {
+ Label.fetchFavoriteLabel(in: context)
+ } else {
+ Label.fetchOrCreate(
+ remoteIdentifier: conversationLabel.id,
+ create: true,
+ in: context,
+ created: &created
+ )
+ }
+
+ guard let label else {
+ throw Failure.failedToStoreLabelLocally(conversationLabel.id)
+ }
+
+ label.name = conversationLabel.name
+ label.kind = Label.Kind(rawValue: conversationLabel.type) ?? .folder
+
+ let conversations = ZMConversation.fetchObjects(
+ withRemoteIdentifiers: Set(conversationLabel.conversationIDs),
+ in: context
+ ) as? Set ?? Set()
+
+ label.conversations = conversations
+ label.modifiedKeys = nil
+
+ do {
+ try context.save()
+ } catch {
+ throw Failure.failedToStoreLabelLocally(conversationLabel.id)
+ }
+ }
+ }
+
+ public func deleteOldLabelsLocally(
+ excludedLabels: [ConversationLabelInfo]
+ ) async throws {
+ try await context.perform { [self] in
+ let uuids = excludedLabels.map { $0.id.uuidData as NSData }
+ let predicateFormat = "type == \(Label.Kind.folder.rawValue) AND NOT remoteIdentifier_data IN %@"
+
+ let predicate = NSPredicate(
+ format: predicateFormat,
+ uuids as CVarArg
+ )
+
+ let fetchRequest: NSFetchRequest
+ fetchRequest = NSFetchRequest(entityName: Label.entityName())
+ fetchRequest.predicate = predicate
+
+ /// Since batch operations bypass the context processing,
+ /// relationships rules are often ignored (e.g delete rule)
+ /// Nevertheless, CoreData automatically handles two specific scenarios:
+ /// `Cascade` delete rule and `Nullify` delete rule on an optional property
+ /// Since `conversations` is nullify and optional, we can safely perform a batch delete.
+
+ let deleteRequest = NSBatchDeleteRequest(
+ fetchRequest: fetchRequest
+ )
+
+ deleteRequest.resultType = .resultTypeObjectIDs
+
+ do {
+ let batchDelete = try context.execute(deleteRequest) as? NSBatchDeleteResult
+
+ guard let deleteResult = batchDelete?.result as? [NSManagedObjectID] else {
+ throw ConversationLabelsRepositoryError.failedToDeleteStoredLabels
+ }
+
+ let deletedObjects: [AnyHashable: Any] = [
+ NSDeletedObjectsKey: deleteResult
+ ]
+
+ /// Since `NSBatchDeleteRequest` only operates at the SQL level (in the persistent store itself),
+ /// we need to manually update our in-memory objects after execution.
+
+ NSManagedObjectContext.mergeChanges(
+ fromRemoteContextSave: deletedObjects,
+ into: [context]
+ )
+
+ } catch {
+ logger.error("Failed to delete old labels: \(error)")
+ throw error
+ }
+ }
+ }
+
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsModelMappings.swift b/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsModelMappings.swift
new file mode 100644
index 00000000000..de1c5bf7783
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsModelMappings.swift
@@ -0,0 +1,31 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireAPI
+
+extension WireAPI.ConversationLabel {
+
+ func toDomainModel() -> ConversationLabelInfo {
+ .init(
+ id: id,
+ name: name,
+ type: type,
+ conversationIDs: conversationIDs
+ )
+ }
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsRepository.swift b/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsRepository.swift
index d2fce7943ae..f5045897c2e 100644
--- a/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsRepository.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsRepository.swift
@@ -16,9 +16,9 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import CoreData
import WireAPI
import WireDataModel
+import WireLogging
// sourcery: AutoMockable
/// Facilitates access to conversation labels related domain objects.
@@ -42,25 +42,21 @@ public class ConversationLabelsRepository: ConversationLabelsRepositoryProtocol
// MARK: - Properties
private let userPropertiesAPI: any UserPropertiesAPI
- // swiftlint:disable:next todo_requires_jira_link
- // TODO: create ConversationLabelsLocalStore
- private let context: NSManagedObjectContext
+ private let conversationLabelsLocalStore: any ConversationLabelsLocalStoreProtocol
private let logger = WireLogger(tag: "conversation-labels")
// MARK: - Object lifecycle
init(
userPropertiesAPI: any UserPropertiesAPI,
- context: NSManagedObjectContext
+ conversationLabelsLocalStore: any ConversationLabelsLocalStoreProtocol
) {
self.userPropertiesAPI = userPropertiesAPI
- self.context = context
+ self.conversationLabelsLocalStore = conversationLabelsLocalStore
}
// MARK: - Public
- /// Retrieve from backend and store conversation labels locally
-
public func pullConversationLabels() async throws {
let conversationLabels = try await userPropertiesAPI.getLabels()
try await updateConversationLabels(conversationLabels)
@@ -70,7 +66,10 @@ public class ConversationLabelsRepository: ConversationLabelsRepositoryProtocol
_ conversationLabels: [ConversationLabel]
) async throws {
await storeLabelsLocally(conversationLabels)
- try await deleteOldLabelsLocally(excludedLabels: conversationLabels)
+
+ try await conversationLabelsLocalStore.deleteOldLabelsLocally(
+ excludedLabels: conversationLabels.map { $0.toDomainModel() }
+ )
}
// MARK: - Private
@@ -81,7 +80,9 @@ public class ConversationLabelsRepository: ConversationLabelsRepositoryProtocol
await withThrowingTaskGroup(of: Void.self) { taskGroup in
for conversationLabel in conversationLabels {
taskGroup.addTask { [self] in
- try await storeLabelLocally(conversationLabel)
+ try await conversationLabelsLocalStore.storeLabel(
+ conversationLabel.toDomainModel()
+ )
}
}
@@ -91,9 +92,12 @@ public class ConversationLabelsRepository: ConversationLabelsRepositoryProtocol
case .success:
continue
case let .failure(error):
- let repoError = error as? ConversationLabelsRepositoryError
- if case let .failedToStoreLabelLocally(label) = repoError {
- logger.error("Failed to store conversation label with id \(label.id): \(error)")
+ let repoError = error as? ConversationLabelsLocalStore.Failure
+ if case let .failedToStoreLabelLocally(id) = repoError {
+ logger
+ .error(
+ "Failed to store conversation label with id \(id.safeForLoggingDescription): \(error)"
+ )
} else {
logger.error("Failed to store conversation with error: \(error)")
}
@@ -101,105 +105,4 @@ public class ConversationLabelsRepository: ConversationLabelsRepositoryProtocol
}
}
}
-
- /// Save label and related conversations objects to local storage.
- /// - Parameter conversationLabel: conversation label from WireAPI
-
- private func storeLabelLocally(
- _ conversationLabel: ConversationLabel
- ) async throws {
- try await context.perform { [context] in
- var created = false
- let label: Label? = if conversationLabel.type == Label.Kind.favorite.rawValue {
- Label.fetchFavoriteLabel(in: context)
- } else {
- Label.fetchOrCreate(
- remoteIdentifier: conversationLabel.id,
- create: true,
- in: context,
- created: &created
- )
- }
-
- guard let label else {
- throw ConversationLabelsRepositoryError.failedToStoreLabelLocally(conversationLabel)
- }
-
- label.name = conversationLabel.name
- label.kind = Label.Kind(rawValue: conversationLabel.type) ?? .folder
-
- let conversations = ZMConversation.fetchObjects(
- withRemoteIdentifiers: Set(conversationLabel.conversationIDs),
- in: context
- ) as? Set ?? Set()
-
- label.conversations = conversations
- label.modifiedKeys = nil
-
- do {
- try context.save()
- } catch {
- throw ConversationLabelsRepositoryError.failedToStoreLabelLocally(conversationLabel)
- }
- }
- }
-
- /// Delete old `folder` labels and related conversations objects from local storage.
- /// - Parameter excludedLabels: remote labels that should be excluded from deletion.
- /// - Only old labels of type `folder` are deleted, `favorite` labels always remain in the local storage.
-
- private func deleteOldLabelsLocally(
- excludedLabels remoteLabels: [ConversationLabel]
- ) async throws {
- try await context.perform { [self] in
- let uuids = remoteLabels.map { $0.id.uuidData as NSData }
- let predicateFormat = "type == \(Label.Kind.folder.rawValue) AND NOT remoteIdentifier_data IN %@"
-
- let predicate = NSPredicate(
- format: predicateFormat,
- uuids as CVarArg
- )
-
- let fetchRequest: NSFetchRequest
- fetchRequest = NSFetchRequest(entityName: Label.entityName())
- fetchRequest.predicate = predicate
-
- /// Since batch operations bypass the context processing,
- /// relationships rules are often ignored (e.g delete rule)
- /// Nevertheless, CoreData automatically handles two specific scenarios:
- /// `Cascade` delete rule and `Nullify` delete rule on an optional property
- /// Since `conversations` is nullify and optional, we can safely perform a batch delete.
-
- let deleteRequest = NSBatchDeleteRequest(
- fetchRequest: fetchRequest
- )
-
- deleteRequest.resultType = .resultTypeObjectIDs
-
- do {
- let batchDelete = try context.execute(deleteRequest) as? NSBatchDeleteResult
-
- guard let deleteResult = batchDelete?.result as? [NSManagedObjectID] else {
- throw ConversationLabelsRepositoryError.failedToDeleteStoredLabels
- }
-
- let deletedObjects: [AnyHashable: Any] = [
- NSDeletedObjectsKey: deleteResult
- ]
-
- /// Since `NSBatchDeleteRequest` only operates at the SQL level (in the persistent store itself),
- /// we need to manually update our in-memory objects after execution.
-
- NSManagedObjectContext.mergeChanges(
- fromRemoteContextSave: deletedObjects,
- into: [context]
- )
-
- } catch {
- logger.error("Failed to delete old labels: \(error)")
- throw error
- }
- }
- }
-
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsRepositoryError.swift b/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsRepositoryError.swift
index d5c17089b81..fa999e54fb7 100644
--- a/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsRepositoryError.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsRepositoryError.swift
@@ -16,16 +16,13 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
+import Foundation
import WireAPI
/// Errors originating from `ConversationLabelsRepository`.
enum ConversationLabelsRepositoryError: Error, Equatable {
- /// Unable to store label locally
-
- case failedToStoreLabelLocally(ConversationLabel)
-
/// Unable to pull labels from backend
case failedToCollectLabelsRemotely
diff --git a/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/Models/ConversationLabelInfo.swift b/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/Models/ConversationLabelInfo.swift
new file mode 100644
index 00000000000..75c956c98ed
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/Models/ConversationLabelInfo.swift
@@ -0,0 +1,26 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+
+public struct ConversationLabelInfo: Sendable {
+ let id: UUID
+ let name: String?
+ let type: Int16
+ let conversationIDs: [UUID]
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/FeatureConfigLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/FeatureConfigLocalStore.swift
new file mode 100644
index 00000000000..e01a1aaf605
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/FeatureConfigLocalStore.swift
@@ -0,0 +1,171 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+import WireLogging
+
+protocol FeatureConfigLocalStoreProtocol {
+
+ /// Fetches a feature locally.
+ /// - parameter name: The name of the feature.
+ /// - returns: The feature found locally
+
+ func fetchFeature(
+ name: Feature.Name
+ ) async throws -> Feature
+
+ /// Stores a flag indicating whether the user needs to be notified of the feature.
+ /// - parameters:
+ /// - needsNotifyUser: The flag to update.
+ /// - feature: The feature to update the flag for.
+
+ func storeFeature(
+ needsNotifyUser: Bool,
+ feature: Feature
+ ) async
+
+ /// Fetches a flag whether the user needs to be notified for the feature.
+ /// - parameter feature: A given feature.
+ /// - returns: Whether the user needs to be notified.
+
+ func featureNeedsNotifyUser(
+ feature: Feature
+ ) async -> Bool
+
+ /// Stores a feature locally.
+ /// - parameters:
+ /// - name: The name of the feature
+ /// - isEnabled: Whether the feature is enabled.
+ /// - config: The config of the feature if any.
+
+ func storeFeature(
+ name: Feature.Name,
+ isEnabled: Bool,
+ config: (any Codable)?
+ ) async
+
+ /// Fetches a feature config info.
+ /// - parameter feature: The feature to fetch the info from.
+ /// - returns: A status (enabled or disabled) and a config payload.
+
+ func featureConfig(
+ feature: Feature
+ ) async -> (status: Feature.Status, config: Data?)
+
+}
+
+final class FeatureConfigLocalStore: FeatureConfigLocalStoreProtocol {
+
+ // MARK: - Error
+
+ enum Error: Swift.Error {
+ case failedToFetchFeatureLocally
+ }
+
+ // MARK: - Properties
+
+ private let context: NSManagedObjectContext
+
+ // MARK: - Object lifecycle
+
+ init(
+ context: NSManagedObjectContext
+ ) {
+ self.context = context
+ }
+
+ // MARK: - Public
+
+ public func fetchFeature(
+ name: Feature.Name
+ ) async throws -> Feature {
+ try await context.perform { [context] in
+ guard let feature = Feature.fetch(
+ name: name,
+ context: context
+ ) else {
+ throw Error.failedToFetchFeatureLocally
+ }
+
+ return feature
+ }
+ }
+
+ public func storeFeature(
+ needsNotifyUser: Bool,
+ feature: Feature
+ ) async {
+ await context.perform {
+ feature.needsToNotifyUser = needsNotifyUser
+ }
+ }
+
+ public func featureConfig(
+ feature: Feature
+ ) async -> (status: Feature.Status, config: Data?) {
+ await context.perform {
+ (feature.status, feature.config)
+ }
+ }
+
+ func featureNeedsNotifyUser(
+ feature: Feature
+ ) async -> Bool {
+ await context.perform {
+ feature.needsToNotifyUser
+ }
+ }
+
+ public func storeFeature(
+ name: Feature.Name,
+ isEnabled: Bool,
+ config: (any Codable)? = nil
+ ) async {
+ await context.perform { [context] in
+ if let config {
+ let encoder = JSONEncoder()
+
+ do {
+ let data = try encoder.encode(config)
+
+ Feature.updateOrCreate(
+ havingName: name,
+ in: context
+ ) {
+ $0.status = isEnabled ? .enabled : .disabled
+ $0.config = data
+ }
+
+ } catch {
+ WireLogger.featureConfigs.error(
+ "Failed to encode \(String(describing: config.self)) : \(error)"
+ )
+ }
+
+ } else {
+ Feature.updateOrCreate(
+ havingName: name,
+ in: context
+ ) {
+ $0.status = isEnabled ? .enabled : .disabled
+ }
+ }
+ }
+ }
+
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/FeatureConfigRepository.swift b/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/FeatureConfigRepository.swift
index 2124c9dae00..ed163b10591 100644
--- a/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/FeatureConfigRepository.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/FeatureConfigRepository.swift
@@ -16,11 +16,10 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import Foundation
-
import Combine
import WireAPI
import WireDataModel
+import WireLogging
/// Facilitates access to feature configs related domain objects.
protocol FeatureConfigRepositoryProtocol {
@@ -48,25 +47,35 @@ protocol FeatureConfigRepositoryProtocol {
/// - Parameter type: The type of config to retrieve.
/// - Returns: A `LocalFeature` object with a status and a config (if any).
- func fetchFeatureConfig(with name: Feature.Name, type: T.Type) async throws -> LocalFeature
+ func fetchFeatureConfig(
+ name: Feature.Name,
+ type: T.Type
+ ) async throws -> LocalFeature
/// Updates a feature config locally.
///
/// - Parameter featureConfig: The feature config to update.
- func updateFeatureConfig(_ featureConfig: FeatureConfig) async throws
+ func updateFeatureConfig(
+ _ featureConfig: FeatureConfig
+ ) async throws
/// Fetches a flag indicating whether the user should be notified of a given feature.
/// - Parameter name: The feature name.
/// - Returns: `true` if user should be notified.
- func fetchNeedsToNotifyUser(for name: Feature.Name) async throws -> Bool
+ func needsToNotifyUser(
+ name: Feature.Name
+ ) async throws -> Bool
/// Stores a flag indicating whether the user should be notified of a given feature.
/// - Parameter notifyUser: Whether the user should be notified for a given feature.
/// - Parameter name: The name of the feature to set the flag for.
- func storeNeedsToNotifyUser(_ notifyUser: Bool, forFeatureName name: Feature.Name) async throws
+ func storeFeatureNeedsToNotifyUser(
+ _ notifyUser: Bool,
+ name: Feature.Name
+ ) async throws
}
@@ -75,9 +84,7 @@ final class FeatureConfigRepository: FeatureConfigRepositoryProtocol {
// MARK: - Properties
private let featureConfigsAPI: any FeatureConfigsAPI
- // swiftlint:disable:next todo_requires_jira_link
- // TODO: create FeatureConfigLocalStore
- private let context: NSManagedObjectContext
+ private let featureConfigLocalStore: any FeatureConfigLocalStoreProtocol
private let logger = WireLogger.featureConfigs
private let featureStateSubject = PassthroughSubject()
@@ -85,10 +92,10 @@ final class FeatureConfigRepository: FeatureConfigRepositoryProtocol {
init(
featureConfigsAPI: any FeatureConfigsAPI,
- context: NSManagedObjectContext
+ featureConfigLocalStore: any FeatureConfigLocalStoreProtocol
) {
self.featureConfigsAPI = featureConfigsAPI
- self.context = context
+ self.featureConfigLocalStore = featureConfigLocalStore
}
// MARK: - Public
@@ -97,8 +104,15 @@ final class FeatureConfigRepository: FeatureConfigRepositoryProtocol {
let featureConfigs = try await featureConfigsAPI.getFeatureConfigs()
for featureConfig in featureConfigs {
- await storeFeatureConfig(featureConfig)
- await sendFeatureState(for: featureConfig)
+ if let featureConfigInfo = getFeatureConfigInfo(featureConfig) {
+ await featureConfigLocalStore.storeFeature(
+ name: featureConfigInfo.name,
+ isEnabled: featureConfigInfo.isEnabled,
+ config: featureConfigInfo.config
+ )
+
+ await sendFeatureState(for: featureConfig)
+ }
}
}
@@ -106,37 +120,67 @@ final class FeatureConfigRepository: FeatureConfigRepositoryProtocol {
featureStateSubject.eraseToAnyPublisher()
}
- func fetchFeatureConfig(with name: Feature.Name, type: T.Type) async throws -> LocalFeature {
- try await context.perform { [self] in
- let feature = try fetchFeature(withName: name)
+ func fetchFeatureConfig(
+ name: Feature.Name,
+ type: T.Type
+ ) async throws -> LocalFeature {
+ let feature = try await featureConfigLocalStore.fetchFeature(
+ name: name
+ )
- if let config = feature.config {
- let decoder = JSONDecoder()
- let config = try decoder.decode(type, from: config)
+ let featureConfig = await featureConfigLocalStore.featureConfig(feature: feature)
- return LocalFeature(status: feature.status, config: config)
- }
+ if let config = featureConfig.config {
+ let decoder = JSONDecoder()
+ let config = try decoder.decode(type, from: config)
- return LocalFeature(status: feature.status, config: nil)
+ return LocalFeature(status: featureConfig.status, config: config)
}
+
+ return LocalFeature(status: featureConfig.status, config: nil)
}
- func fetchNeedsToNotifyUser(for name: Feature.Name) async throws -> Bool {
- try await context.perform { [self] in
- let feature = try fetchFeature(withName: name)
- return feature.needsToNotifyUser
- }
+ func needsToNotifyUser(
+ name: Feature.Name
+ ) async throws -> Bool {
+ let feature = try await featureConfigLocalStore.fetchFeature(
+ name: name
+ )
+
+ return await featureConfigLocalStore.featureNeedsNotifyUser(
+ feature: feature
+ )
}
- func storeNeedsToNotifyUser(_ notifyUser: Bool, forFeatureName name: Feature.Name) async throws {
- try await context.perform { [self] in
- let feature = try fetchFeature(withName: name)
- feature.needsToNotifyUser = notifyUser
- }
+ func storeFeatureNeedsToNotifyUser(
+ _ notifyUser: Bool,
+ name: Feature.Name
+ ) async throws {
+ let feature = try await featureConfigLocalStore.fetchFeature(
+ name: name
+ )
+
+ await featureConfigLocalStore.storeFeature(
+ needsNotifyUser: notifyUser,
+ feature: feature
+ )
}
- func updateFeatureConfig(_ featureConfig: FeatureConfig) async throws {
- await storeFeatureConfig(featureConfig)
+ func updateFeatureConfig(
+ _ featureConfig: FeatureConfig
+ ) async throws {
+ guard let featureConfigInfo = getFeatureConfigInfo(
+ featureConfig
+ ) else {
+ return
+ }
+
+ await featureConfigLocalStore.storeFeature(
+ name: featureConfigInfo.name,
+ isEnabled: featureConfigInfo.isEnabled,
+ config: featureConfigInfo.config
+ )
+
await sendFeatureState(for: featureConfig)
}
@@ -150,21 +194,13 @@ final class FeatureConfigRepository: FeatureConfigRepositoryProtocol {
featureStateSubject.send(featureState)
}
- private func fetchFeature(withName name: Feature.Name) throws -> Feature {
- guard let feature = Feature.fetch(name: name, context: context) else {
- throw FeatureConfigRepositoryError.failedToFetchFeatureLocally
- }
-
- return feature
- }
-
private func getFeatureState(forFeatureConfig config: FeatureConfig) async throws -> FeatureState? {
switch config {
case let .appLock(appLockFeatureConfig):
return FeatureState(
name: .appLock,
- status: appLockFeatureConfig.status,
+ isEnabled: appLockFeatureConfig.status == .enabled,
shouldNotifyUser: false
)
@@ -172,25 +208,25 @@ final class FeatureConfigRepository: FeatureConfigRepositoryProtocol {
return FeatureState(
name: .classifiedDomains,
- status: classifiedDomainsFeatureConfig.status,
+ isEnabled: classifiedDomainsFeatureConfig.status == .enabled,
shouldNotifyUser: false
)
case let .conferenceCalling(conferenceCallingFeatureConfig):
- let needsToNotifyUser = try await fetchNeedsToNotifyUser(for: .conferenceCalling)
+ let needsToNotifyUser = try await needsToNotifyUser(name: .conferenceCalling)
return FeatureState(
name: .conferenceCalling,
- status: conferenceCallingFeatureConfig.status,
+ isEnabled: conferenceCallingFeatureConfig.status == .enabled,
shouldNotifyUser: needsToNotifyUser
)
case let .conversationGuestLinks(conversationGuestLinksFeatureConfig):
- let needsToNotifyUser = try await fetchNeedsToNotifyUser(for: .conversationGuestLinks)
+ let needsToNotifyUser = try await needsToNotifyUser(name: .conversationGuestLinks)
return FeatureState(
name: .conversationGuestLinks,
- status: conversationGuestLinksFeatureConfig.status,
+ isEnabled: conversationGuestLinksFeatureConfig.status == .enabled,
shouldNotifyUser: needsToNotifyUser
)
@@ -198,7 +234,7 @@ final class FeatureConfigRepository: FeatureConfigRepositoryProtocol {
return FeatureState(
name: .digitalSignature,
- status: digitalSignatureFeatureConfig.status,
+ isEnabled: digitalSignatureFeatureConfig.status == .enabled,
shouldNotifyUser: false
)
@@ -206,16 +242,16 @@ final class FeatureConfigRepository: FeatureConfigRepositoryProtocol {
return FeatureState(
name: .e2ei,
- status: endToEndIdentityFeatureConfig.status,
+ isEnabled: endToEndIdentityFeatureConfig.status == .enabled,
shouldNotifyUser: false
)
case let .fileSharing(fileSharingFeatureConfig):
- let needsToNotifyUser = try await fetchNeedsToNotifyUser(for: .fileSharing)
+ let needsToNotifyUser = try await needsToNotifyUser(name: .fileSharing)
return FeatureState(
name: .fileSharing,
- status: fileSharingFeatureConfig.status,
+ isEnabled: fileSharingFeatureConfig.status == .enabled,
shouldNotifyUser: needsToNotifyUser
)
@@ -223,7 +259,7 @@ final class FeatureConfigRepository: FeatureConfigRepositoryProtocol {
return FeatureState(
name: .mls,
- status: mlsFeatureConfig.status,
+ isEnabled: mlsFeatureConfig.status == .enabled,
shouldNotifyUser: false
)
@@ -231,16 +267,16 @@ final class FeatureConfigRepository: FeatureConfigRepositoryProtocol {
return FeatureState(
name: .mlsMigration,
- status: mLSMigrationFeatureConfig.status,
+ isEnabled: mLSMigrationFeatureConfig.status == .enabled,
shouldNotifyUser: false
)
case let .selfDeletingMessages(selfDeletingMessagesFeatureConfig):
- let needsToNotifyUser = try await fetchNeedsToNotifyUser(for: .selfDeletingMessages)
+ let needsToNotifyUser = try await needsToNotifyUser(name: .selfDeletingMessages)
return FeatureState(
name: .selfDeletingMessages,
- status: selfDeletingMessagesFeatureConfig.status,
+ isEnabled: selfDeletingMessagesFeatureConfig.status == .enabled,
shouldNotifyUser: needsToNotifyUser
)
@@ -253,144 +289,97 @@ final class FeatureConfigRepository: FeatureConfigRepositoryProtocol {
}
}
- private func storeFeatureConfig(_ featureConfig: FeatureConfig) async {
- await context.perform { [self] in
-
- switch featureConfig {
- case let .appLock(appLockFeatureConfig):
-
- updateOrCreate(
- featureName: .appLock,
- isEnabled: appLockFeatureConfig.status == .enabled,
- config: appLockFeatureConfig.toDomainModel()
- )
-
- case let .classifiedDomains(classifiedDomainsFeatureConfig):
+ private func getFeatureConfigInfo(
+ _ featureConfig: FeatureConfig
+ ) -> (name: Feature.Name, isEnabled: Bool, config: (any Codable)?)? {
+ switch featureConfig {
+ case let .appLock(appLockFeatureConfig):
- updateOrCreate(
- featureName: .classifiedDomains,
- isEnabled: classifiedDomainsFeatureConfig.status == .enabled,
- config: classifiedDomainsFeatureConfig.toDomainModel()
- )
+ return (
+ .appLock,
+ appLockFeatureConfig.status == .enabled,
+ appLockFeatureConfig.toDomainModel()
+ )
- case let .conferenceCalling(conferenceCallingFeatureConfig):
+ case let .classifiedDomains(classifiedDomainsFeatureConfig):
- updateOrCreate(
- featureName: .conferenceCalling,
- isEnabled: conferenceCallingFeatureConfig.status == .enabled,
- config: conferenceCallingFeatureConfig.toDomainModel() /// always nil for api < v6
- )
+ return (
+ .classifiedDomains,
+ classifiedDomainsFeatureConfig.status == .enabled,
+ classifiedDomainsFeatureConfig.toDomainModel()
+ )
- case let .conversationGuestLinks(conversationGuestLinksFeatureConfig):
+ case let .conferenceCalling(conferenceCallingFeatureConfig):
- updateOrCreate(
- featureName: .conversationGuestLinks,
- isEnabled: conversationGuestLinksFeatureConfig.status == .enabled
- )
+ return (
+ .conferenceCalling,
+ conferenceCallingFeatureConfig.status == .enabled,
+ conferenceCallingFeatureConfig.toDomainModel()
+ )
- case let .digitalSignature(digitalSignatureFeatureConfig):
+ case let .conversationGuestLinks(conversationGuestLinksFeatureConfig):
- updateOrCreate(
- featureName: .digitalSignature,
- isEnabled: digitalSignatureFeatureConfig.status == .enabled
- )
+ return (
+ .conversationGuestLinks,
+ conversationGuestLinksFeatureConfig.status == .enabled,
+ nil
+ )
- case let .endToEndIdentity(endToEndIdentityFeatureConfig):
+ case let .digitalSignature(digitalSignatureFeatureConfig):
- updateOrCreate(
- featureName: .e2ei,
- isEnabled: endToEndIdentityFeatureConfig.status == .enabled,
- config: endToEndIdentityFeatureConfig.toDomainModel()
- )
+ return (
+ .digitalSignature,
+ digitalSignatureFeatureConfig.status == .enabled,
+ nil
+ )
- case let .fileSharing(fileSharingFeatureConfig):
+ case let .endToEndIdentity(endToEndIdentityFeatureConfig):
- updateOrCreate(
- featureName: .fileSharing,
- isEnabled: fileSharingFeatureConfig.status == .enabled
- )
+ return (
+ .e2ei,
+ endToEndIdentityFeatureConfig.status == .enabled,
+ nil
+ )
- case let .mls(mlsFeatureConfig):
+ case let .fileSharing(fileSharingFeatureConfig):
- updateOrCreate(
- featureName: .mls,
- isEnabled: mlsFeatureConfig.status == .enabled,
- config: mlsFeatureConfig.toDomainModel()
- )
+ return (
+ .fileSharing,
+ fileSharingFeatureConfig.status == .enabled,
+ nil
+ )
- case let .mlsMigration(mLSMigrationFeatureConfig):
+ case let .mls(mlsFeatureConfig):
- updateOrCreate(
- featureName: .mlsMigration,
- isEnabled: mLSMigrationFeatureConfig.status == .enabled,
- config: mLSMigrationFeatureConfig.toDomainModel()
- )
+ return (
+ .mls,
+ mlsFeatureConfig.status == .enabled,
+ mlsFeatureConfig.toDomainModel()
+ )
- case let .selfDeletingMessages(selfDeletingMessagesFeatureConfig):
+ case let .mlsMigration(mLSMigrationFeatureConfig):
- updateOrCreate(
- featureName: .selfDeletingMessages,
- isEnabled: selfDeletingMessagesFeatureConfig.status == .enabled,
- config: selfDeletingMessagesFeatureConfig.toDomainModel()
- )
+ return (
+ .mlsMigration,
+ mLSMigrationFeatureConfig.status == .enabled,
+ mLSMigrationFeatureConfig.toDomainModel()
+ )
- case let .unknown(featureName):
+ case let .selfDeletingMessages(selfDeletingMessagesFeatureConfig):
- logger.warn(
- "Unknown feature name: \(featureName)"
- )
- }
- }
- }
+ return (
+ .selfDeletingMessages,
+ selfDeletingMessagesFeatureConfig.status == .enabled,
+ selfDeletingMessagesFeatureConfig.toDomainModel()
+ )
- private func updateOrCreate(
- featureName: Feature.Name,
- isEnabled: Bool,
- config: (any Codable)? = nil
- ) {
- if let config {
- let encoder = JSONEncoder()
-
- do {
- let data = try encoder.encode(config)
-
- Feature.updateOrCreate(
- havingName: featureName,
- in: context
- ) {
- $0.status = isEnabled ? .enabled : .disabled
- $0.config = data
- }
-
- } catch {
- logger.error(
- "Failed to encode \(String(describing: config.self)) : \(error)"
- )
- }
+ case let .unknown(featureName):
+ logger.warn(
+ "Unknown feature name: \(featureName)"
+ )
- } else {
- Feature.updateOrCreate(
- havingName: featureName,
- in: context
- ) {
- $0.status = isEnabled ? .enabled : .disabled
- }
+ return nil
}
}
}
-
-/// A feature fetched locally
-
-struct LocalFeature {
- let status: Feature.Status
- let config: T?
-}
-
-/// The state of the feature
-
-struct FeatureState {
- let name: Feature.Name
- let status: FeatureConfigStatus
- let shouldNotifyUser: Bool
-}
diff --git a/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/Models/FeatureState.swift b/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/Models/FeatureState.swift
new file mode 100644
index 00000000000..8a50b6ce260
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/Models/FeatureState.swift
@@ -0,0 +1,27 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+
+/// The state of the feature
+
+struct FeatureState {
+ let name: Feature.Name
+ let isEnabled: Bool
+ let shouldNotifyUser: Bool
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/Models/LocalFeature.swift b/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/Models/LocalFeature.swift
new file mode 100644
index 00000000000..a090534cf91
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/FeatureConfig/Models/LocalFeature.swift
@@ -0,0 +1,25 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+
+/// A feature fetched locally
+struct LocalFeature {
+ let status: Feature.Status
+ let config: T?
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Message/MessageLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/Message/MessageLocalStore.swift
index df7e5860432..8c231ff56ef 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Message/MessageLocalStore.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Message/MessageLocalStore.swift
@@ -16,7 +16,6 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import CoreData
import WireDataModel
// sourcery: AutoMockable
@@ -37,15 +36,18 @@ public final class MessageLocalStore: MessageLocalStoreProtocol {
let context: NSManagedObjectContext
let conversationLocalStore: any ConversationLocalStoreProtocol
+ let userLocalStore: any UserLocalStoreProtocol
// MARK: - Object lifecycle
public init(
context: NSManagedObjectContext,
- conversationLocalStore: any ConversationLocalStoreProtocol
+ conversationLocalStore: any ConversationLocalStoreProtocol,
+ userLocalStore: any UserLocalStoreProtocol
) {
self.context = context
self.conversationLocalStore = conversationLocalStore
+ self.userLocalStore = userLocalStore
}
// MARK: - Public
@@ -114,6 +116,54 @@ public final class MessageLocalStore: MessageLocalStoreProtocol {
return [systemMessage]
+ case let .participantsRemoved(participants, sender, date):
+
+ let removedUsers = await context.perform {
+ participants.compactMap { id, domain in
+ let existing = conversation.localParticipants
+
+ return existing.first(where: {
+ $0.remoteIdentifier == id && $0.domain == domain
+ })
+ }
+ }
+
+ guard let sender = await fetchUser(
+ id: sender.id,
+ domain: sender.domain
+ ) else {
+ return []
+ }
+
+ let systemMessage = await createSystemMessage(
+ messageType: .participantsRemoved,
+ sender: sender,
+ users: Set(removedUsers),
+ timestamp: date
+ )
+
+ return [systemMessage]
+
+ case let .participantsAdded(participants, sender, date):
+ guard let sender = await fetchUser(
+ id: sender.id,
+ domain: sender.domain
+ ) else {
+ return []
+ }
+
+ let newUsers = await userLocalStore.fetchOrCreateUsers(
+ userIDs: participants
+ )
+
+ let systemMessage = await createSystemMessage(
+ messageType: .participantsAdded,
+ sender: sender,
+ users: newUsers
+ )
+
+ return [systemMessage]
+
case .mlsMigrationMLSNotSupportedForSelfUser:
let selfUser = await fetchSelfUser()
@@ -159,27 +209,6 @@ public final class MessageLocalStore: MessageLocalStoreProtocol {
return [systemMessage]
- case let .participantRemoved(participant, sender, date):
-
- guard let removedParticipant = await fetchUser(
- id: participant.id,
- domain: participant.domain
- ) else { return [] }
-
- let sender = await fetchUser(
- id: sender.id,
- domain: sender.domain
- )
-
- let systemMessage = await createSystemMessage(
- messageType: .participantsRemoved,
- sender: sender ?? removedParticipant,
- users: Set([removedParticipant]),
- timestamp: date
- )
-
- return [systemMessage]
-
case let .newConversationCreated(date):
let selfUser = await fetchSelfUser()
@@ -312,6 +341,72 @@ public final class MessageLocalStore: MessageLocalStoreProtocol {
timestamp: date
)
+ return [systemMessage]
+
+ case let .messageTimerUpdate(sender, date, timeoutValue):
+
+ guard let sender = await fetchUser(
+ id: sender.id,
+ domain: sender.domain
+ ) else {
+ return []
+ }
+
+ let systemMessage = await createSystemMessage(
+ messageType: .messageTimerUpdate,
+ sender: sender,
+ users: [sender],
+ timestamp: date,
+ messageTimer: timeoutValue
+ )
+
+ return [systemMessage]
+
+ case let .conversationNameChanged(newName, sender, date):
+ guard let sender = await fetchUser(
+ id: sender.id,
+ domain: sender.domain
+ ) else {
+ return []
+ }
+
+ let systemMessage = await createSystemMessage(
+ messageType: .conversationNameChanged,
+ sender: sender,
+ timestamp: date
+ )
+
+ await context.perform {
+ systemMessage.text = newName
+ systemMessage.visibleInConversation = conversation
+ conversation.updateTimestampsAfterUpdatingMessage(systemMessage)
+ }
+
+ return [systemMessage]
+
+ case let .readReceiptsStatus(isEnabled, sender, date):
+ guard let sender = await fetchUser(
+ id: sender.id,
+ domain: sender.domain
+ ) else {
+ return []
+ }
+
+ let systemMessage = await createSystemMessage(
+ messageType: isEnabled ? .readReceiptsEnabled : .readReceiptsDisabled,
+ sender: sender,
+ timestamp: date
+ )
+
+ await context.perform {
+ let isArchived = conversation.isArchived
+ let mutedMessageTypes = conversation.mutedMessageTypes
+
+ if isArchived, mutedMessageTypes == .none {
+ conversation.isArchived = false
+ }
+ }
+
return [systemMessage]
}
}
@@ -365,24 +460,17 @@ public final class MessageLocalStore: MessageLocalStoreProtocol {
}
}
- // swiftlint:disable:next todo_requires_jira_link
- // TODO: Use UserLocalStore when related PRs are merged.
private func fetchUser(
id: UUID,
domain: String?
) async -> ZMUser? {
- await context.perform { [context] in
- ZMUser.fetch(
- with: id,
- domain: domain,
- in: context
- )
- }
+ try? await userLocalStore.fetchUser(
+ id: id,
+ domain: domain
+ )
}
private func fetchSelfUser() async -> ZMUser {
- await context.perform { [context] in
- ZMUser.selfUser(in: context)
- }
+ await userLocalStore.fetchSelfUser()
}
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Message/Models/MessageType.swift b/WireDomain/Sources/WireDomain/Repositories/Message/Models/MessageType.swift
index 352e197b3e4..ecb7c0ae782 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Message/Models/MessageType.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Message/Models/MessageType.swift
@@ -29,6 +29,18 @@ public enum MessageType: Sendable {
date: Date
)
+ case participantsRemoved(
+ participants: [(id: UUID, domain: String?)],
+ sender: (id: UUID, domain: String?),
+ date: Date
+ )
+
+ case participantsAdded(
+ participants: [(id: UUID, domain: String?)],
+ sender: (id: UUID, domain: String?),
+ date: Date
+ )
+
case mlsMigrationMLSNotSupportedForSelfUser
case mlsMigrationMLSNotSupportedForOtherUser(
@@ -40,13 +52,13 @@ public enum MessageType: Sendable {
date: Date
)
- case participantRemoved(
- participant: (id: UUID, domain: String?),
- sender: (id: UUID, domain: String?),
+ case newConversationCreated(
date: Date
)
- case newConversationCreated(
+ case conversationNameChanged(
+ newName: String,
+ sender: (id: UUID, domain: String?),
date: Date
)
@@ -68,4 +80,16 @@ public enum MessageType: Sendable {
case receiptModeIsOn(
date: Date
)
+
+ case messageTimerUpdate(
+ sender: (id: UUID, domain: String?),
+ date: Date,
+ timeoutValue: Double
+ )
+
+ case readReceiptsStatus(
+ isEnabled: Bool,
+ sender: (id: UUID, domain: String?),
+ date: Date
+ )
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Team/Models/TeamMemberInfo.swift b/WireDomain/Sources/WireDomain/Repositories/Team/Models/TeamMemberInfo.swift
new file mode 100644
index 00000000000..4247ad4c9cb
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/Team/Models/TeamMemberInfo.swift
@@ -0,0 +1,26 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+
+public struct TeamMemberInfo: Sendable {
+ let id: UUID
+ let selfPermission: Int64?
+ let creatorID: UUID?
+ let creationDate: Date?
+}
diff --git a/WireAnalytics/Sources/WireAnalytics/Countly/Countly+Types.swift b/WireDomain/Sources/WireDomain/Repositories/Team/Models/TeamRoleInfo.swift
similarity index 86%
rename from WireAnalytics/Sources/WireAnalytics/Countly/Countly+Types.swift
rename to WireDomain/Sources/WireDomain/Repositories/Team/Models/TeamRoleInfo.swift
index 1ebe660b533..64dce169b51 100644
--- a/WireAnalytics/Sources/WireAnalytics/Countly/Countly+Types.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Team/Models/TeamRoleInfo.swift
@@ -16,8 +16,9 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import Countly
+import Foundation
-public typealias WireCountly = Countly
-
-public typealias WireCountlyConfig = CountlyConfig
+public struct TeamRoleInfo: Sendable {
+ let role: String
+ let actions: [String]
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Team/TeamLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/Team/TeamLocalStore.swift
new file mode 100644
index 00000000000..add4f74b6fa
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/Team/TeamLocalStore.swift
@@ -0,0 +1,319 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+
+// sourcery: AutoMockable
+public protocol TeamLocalStoreProtocol {
+
+ func fetchMember(
+ id: UUID
+ ) async -> Member?
+
+ /// Fetches the self user ID.
+ /// - returns: The self user ID.
+
+ func selfUserID() async -> UUID
+
+ /// Fetches the user membership.
+ /// - parameter user: A given user.
+ /// - returns: The user membership.
+
+ func userMembership(
+ user: ZMUser
+ ) async -> Member?
+
+ /// Fetches the user domain.
+ /// - parameter user: A given user.
+ /// - returns: The user domain.
+
+ func userDomain(
+ user: ZMUser
+ ) async -> String?
+
+ /// Deletes the member locally.
+ /// - parameter member: A given member.
+
+ func deleteMember(
+ _ member: Member
+ ) async
+
+ /// Stores a flag whether the member needs backend update.
+ /// - parameters:
+ /// - needsBackendUpdate: The flag to update.
+ /// - member: A given member.
+
+ func storeMember(
+ needsBackendUpdate: Bool,
+ member: Member
+ ) async
+
+ /// Stores a team locally.
+ /// - Parameters:
+ /// - id: The team ID.
+ /// - name: The team name.
+ /// - creatorID: The team creator ID.
+ /// - logoID: The team logo ID.
+ /// - logoKey: The team logo key.
+
+ func storeTeam(
+ id: UUID,
+ name: String,
+ creatorID: UUID,
+ logoID: String?,
+ logoKey: String?
+ ) async
+
+ /// Stores team roles locally.
+ /// - parameters:
+ /// - selfTeamID: The self team ID.
+ /// - teamRolesInfo: A list of role and actions.
+
+ func storeTeamRoles(
+ selfTeamID: UUID,
+ teamRolesInfo: [TeamRoleInfo]
+ ) async throws
+
+ /// Stores team members locally.
+ /// - parameters:
+ /// - selfTeamID: The self team ID.
+ /// - teamMembersInfo: A list of member info (id, permission, creator id, date)
+
+ func storeTeamMembers(
+ selfTeamID: UUID,
+ teamMembersInfo: [TeamMemberInfo]
+ ) async throws
+
+ /// Fetches self user info : user ID and client ID.
+ /// - returns: the user ID and the client ID.
+
+ func selfUserInfo() async -> (id: UUID, clientId: String?)
+}
+
+public final class TeamLocalStore: TeamLocalStoreProtocol {
+
+ // MARK: - Error
+
+ enum Error: Swift.Error {
+ /// The local team instance was not found in the database.
+
+ case teamNotFoundLocally
+ }
+
+ // MARK: - Properties
+
+ private let context: NSManagedObjectContext
+ private let userLocalStore: any UserLocalStoreProtocol
+
+ // MARK: - Object lifecycle
+
+ public init(
+ context: NSManagedObjectContext,
+ userLocalStore: any UserLocalStoreProtocol
+ ) {
+ self.context = context
+ self.userLocalStore = userLocalStore
+ }
+
+ // MARK: - Public
+
+ public func fetchMember(
+ id: UUID
+ ) async -> Member? {
+ await context.perform { [context] in
+ Member.fetch(
+ with: id,
+ in: context
+ )
+ }
+ }
+
+ public func selfUserID() async -> UUID {
+ let selfUser = await userLocalStore.fetchSelfUser()
+
+ return await context.perform {
+ selfUser.remoteIdentifier
+ }
+ }
+
+ public func userMembership(
+ user: ZMUser
+ ) async -> Member? {
+ await context.perform {
+ user.membership
+ }
+ }
+
+ public func userDomain(
+ user: ZMUser
+ ) async -> String? {
+ await context.perform {
+ user.domain
+ }
+ }
+
+ public func deleteMember(
+ _ member: Member
+ ) async {
+ await context.perform { [context] in
+ context.delete(member)
+ }
+ }
+
+ public func storeMember(
+ needsBackendUpdate: Bool,
+ member: Member
+ ) async {
+ await context.perform { [context] in
+ member.needsToBeUpdatedFromBackend = true
+ context.saveOrRollback()
+ }
+ }
+
+ public func storeTeam(
+ id: UUID,
+ name: String,
+ creatorID: UUID,
+ logoID: String?,
+ logoKey: String?
+ ) async {
+ let selfUser = await userLocalStore.fetchSelfUser()
+
+ await context.perform { [context] in
+ let team = WireDataModel.Team.fetchOrCreate(
+ with: id,
+ in: context
+ )
+
+ _ = WireDataModel.Member.getOrUpdateMember(
+ for: selfUser,
+ in: team,
+ context: context
+ )
+
+ team.name = name
+ team.creator = ZMUser.fetchOrCreate(
+ with: creatorID,
+ domain: nil,
+ in: context
+ )
+ team.pictureAssetId = logoID
+ team.pictureAssetKey = logoKey
+ team.needsToBeUpdatedFromBackend = false
+ }
+ }
+
+ public func storeTeamRoles(
+ selfTeamID: UUID,
+ teamRolesInfo: [TeamRoleInfo]
+ ) async throws {
+ try await context.perform { [context, selfTeamID] in
+ guard let team = WireDataModel.Team.fetch(
+ with: selfTeamID,
+ in: context
+ ) else {
+ throw Error.teamNotFoundLocally
+ }
+
+ let existingRoles = team.roles
+
+ let localRoles = teamRolesInfo.map { teamRoleInfo in
+ let localRole = WireDataModel.Role.fetchOrCreate(
+ name: teamRoleInfo.role,
+ teamOrConversation: .team(team),
+ context: context
+ )
+
+ localRole.name = teamRoleInfo.role
+ localRole.team = team
+
+ for action in teamRoleInfo.actions {
+ let action = Action.fetchOrCreate(
+ name: action,
+ in: context
+ )
+
+ localRole.actions.insert(action)
+ }
+
+ return localRole
+ }
+
+ for roleToDelete in existingRoles.subtracting(localRoles) {
+ context.delete(roleToDelete)
+ }
+
+ team.needsToDownloadRoles = false
+ }
+ }
+
+ public func storeTeamMembers(
+ selfTeamID: UUID,
+ teamMembersInfo: [TeamMemberInfo]
+ ) async throws {
+ try await context.perform { [context, selfTeamID] in
+ guard let team = WireDataModel.Team.fetch(
+ with: selfTeamID,
+ in: context
+ ) else {
+ throw Error.teamNotFoundLocally
+ }
+
+ for teamMemberInfo in teamMembersInfo {
+ let user = ZMUser.fetchOrCreate(
+ with: teamMemberInfo.id,
+ domain: nil,
+ in: context
+ )
+
+ let membership = Member.getOrUpdateMember(
+ for: user,
+ in: team,
+ context: context
+ )
+
+ if let selfPermission = teamMemberInfo.selfPermission {
+ membership.permissions = Permissions(rawValue: selfPermission)
+ }
+
+ if let creatorID = teamMemberInfo.creatorID {
+ membership.createdBy = ZMUser.fetchOrCreate(
+ with: creatorID,
+ domain: nil,
+ in: context
+ )
+ }
+
+ membership.createdAt = teamMemberInfo.creationDate
+ membership.needsToBeUpdatedFromBackend = false
+ }
+ }
+ }
+
+ public func selfUserInfo() async -> (id: UUID, clientId: String?) {
+ let selfUser = await userLocalStore.fetchSelfUser()
+
+ return await context.perform {
+ (
+ id: selfUser.remoteIdentifier,
+ clientId: selfUser.selfClient()?.remoteIdentifier
+ )
+ }
+ }
+
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Team/TeamModelMappings.swift b/WireDomain/Sources/WireDomain/Repositories/Team/TeamModelMappings.swift
new file mode 100644
index 00000000000..c4ca824f569
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/Team/TeamModelMappings.swift
@@ -0,0 +1,70 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireAPI
+
+extension WireAPI.ConversationRole {
+
+ func toDomainModel() -> TeamRoleInfo {
+ .init(
+ role: name,
+ actions: actions.map(\.name)
+ )
+ }
+
+}
+
+extension WireAPI.TeamMember {
+
+ func toDomainModel() -> TeamMemberInfo {
+ .init(
+ id: userID,
+ selfPermission: permissions?.selfPermissions,
+ creatorID: creatorID,
+ creationDate: creationDate
+ )
+ }
+
+}
+
+private extension WireAPI.ConversationAction {
+
+ var name: String {
+ switch self {
+ case .addConversationMember:
+ "add_conversation_member"
+ case .removeConversationMember:
+ "remove_conversation_member"
+ case .modifyConversationName:
+ "modify_conversation_name"
+ case .modifyConversationMessageTimer:
+ "modify_conversation_message_timer"
+ case .modifyConversationReceiptMode:
+ "modify_conversation_receipt_mode"
+ case .modifyConversationAccess:
+ "modify_conversation_access"
+ case .modifyOtherConversationMember:
+ "modify_other_conversation_member"
+ case .leaveConversation:
+ "leave_conversation"
+ case .deleteConversation:
+ "delete_conversation"
+ }
+ }
+
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift
index 3d2f646dc6c..3f0d77f6bc8 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift
@@ -40,25 +40,32 @@ public protocol TeamRepositoryProtocol {
func pullSelfTeamMembers() async throws
- /// Fetch the legalhold status for the self user from the server.
+ /// Fetches the legalhold info for the self user from the server.
+ /// - returns: The legalhold info.
- func fetchSelfLegalholdStatus() async throws -> LegalholdStatus
+ func fetchSelfLegalholdInfo() async throws -> TeamMemberLegalholdInfo
/// Deletes the member of a team.
/// - Parameter userID: The ID of the team member.
/// - Parameter domain: The domain of the team member.
- /// - Parameter time: The time the member left the team.
+ /// - Parameter date: The time the member left the team.
func deleteMembership(
- for userID: UUID,
+ userID: UUID,
domain: String?,
- at time: Date
+ date: Date
) async throws
/// Sets the team member `needsToBeUpdatedFromBackend` flag to true.
/// - Parameter membershipID: The id of the team member.
- func storeTeamMemberNeedsBackendUpdate(membershipID: UUID) async throws
+ func storeTeamMemberNeedsBackendUpdate(
+ membershipID: UUID
+ ) async throws
+
+ /// Pulls and stores legalhold info locally.
+
+ func pullSelfLegalholdInfo() async throws
}
@@ -69,103 +76,147 @@ public class TeamRepository: TeamRepositoryProtocol {
private let selfTeamID: UUID
private let userRepository: any UserRepositoryProtocol
private let teamsAPI: any TeamsAPI
- // swiftlint:disable:next todo_requires_jira_link
- // TODO: create TeamLocalStore
- private let context: NSManagedObjectContext
+ private let teamLocalStore: any TeamLocalStoreProtocol
// MARK: - Object lifecycle
public init(
selfTeamID: UUID,
userRepository: any UserRepositoryProtocol,
- teamsAPI: any TeamsAPI,
- context: NSManagedObjectContext
+ teamLocalStore: any TeamLocalStoreProtocol,
+ teamsAPI: any TeamsAPI
) {
self.selfTeamID = selfTeamID
self.userRepository = userRepository
+ self.teamLocalStore = teamLocalStore
self.teamsAPI = teamsAPI
- self.context = context
}
// MARK: - Public
public func pullSelfTeam() async throws {
let team = try await fetchSelfTeamRemotely()
- await storeTeamLocally(team)
+
+ await teamLocalStore.storeTeam(
+ id: team.id,
+ name: team.name,
+ creatorID: team.creatorID,
+ logoID: team.logoID,
+ logoKey: team.logoKey
+ )
}
public func pullSelfTeamRoles() async throws {
let teamRoles = try await fetchSelfTeamRolesRemotely()
- try await storeTeamRolesLocally(teamRoles)
+
+ let teamRolesInfo = teamRoles.map {
+ $0.toDomainModel()
+ }
+
+ try await teamLocalStore.storeTeamRoles(
+ selfTeamID: selfTeamID,
+ teamRolesInfo: teamRolesInfo
+ )
}
public func pullSelfTeamMembers() async throws {
let teamMembers = try await fetchSelfTeamMembersRemotely()
- try await storeTeamMembersLocally(teamMembers)
+
+ let teamMembersInfo = teamMembers.map {
+ $0.toDomainModel()
+ }
+
+ try await teamLocalStore.storeTeamMembers(
+ selfTeamID: selfTeamID,
+ teamMembersInfo: teamMembersInfo
+ )
}
public func fetchSelfLegalholdStatus() async throws -> LegalholdStatus {
- let selfUser = await userRepository.fetchSelfUser()
+ let selfUserID = await teamLocalStore.selfUserID()
- let selfUserID: UUID = await context.perform {
- selfUser.remoteIdentifier
- }
-
- return try await teamsAPI.getLegalholdStatus(
+ return try await teamsAPI.getLegalholdInfo(
for: selfTeamID,
userID: selfUserID
- )
+ ).status
}
public func deleteMembership(
- for userID: UUID,
+ userID: UUID,
domain: String?,
- at time: Date
+ date: Date
) async throws {
let user = try await userRepository.fetchUser(
id: userID,
domain: domain
)
- let member = try await context.perform {
- guard let member = user.membership else {
- throw TeamRepositoryError.userNotAMemberInTeam(user: userID)
- }
-
- return member
+ guard let member = await teamLocalStore.userMembership(
+ user: user
+ ) else {
+ throw TeamRepositoryError.userNotAMemberInTeam(user: userID)
}
- let domain = await context.perform {
- user.domain
- }
+ let domain = await teamLocalStore.userDomain(user: user)
try await userRepository.deleteUserAccount(
id: userID,
domain: domain,
- at: time
+ at: date
)
- await context.perform { [context] in
- context.delete(member)
- }
+ await teamLocalStore.deleteMember(member)
}
public func storeTeamMemberNeedsBackendUpdate(membershipID: UUID) async throws {
- try await context.perform { [context] in
+ guard let member = await teamLocalStore.fetchMember(
+ id: membershipID
+ ) else {
+ throw TeamRepositoryError.failedToFindTeamMember(membershipID)
+ }
+
+ await teamLocalStore.storeMember(
+ needsBackendUpdate: true,
+ member: member
+ )
+ }
+
+ public func pullSelfLegalholdInfo() async throws {
+ let selfUser = await userRepository.fetchSelfUser()
- guard let member = Member.fetch(
- with: membershipID,
- in: context
- ) else {
- throw TeamRepositoryError.failedToFindTeamMember(membershipID)
+ let (selfUserID, selfClientID) = await teamLocalStore.selfUserInfo()
+
+ let selfUserLegalHold = try await fetchSelfLegalholdInfo()
+
+ switch selfUserLegalHold.status {
+ case .pending:
+ guard let selfClientID else {
+ return
}
- member.needsToBeUpdatedFromBackend = true
+ await userRepository.addLegalHoldRequest(
+ userID: selfUserID,
+ clientID: selfClientID,
+ lastPrekey: selfUserLegalHold.prekey
+ )
- try context.save()
+ case .disabled:
+ await userRepository.disableUserLegalHold()
+
+ default:
+ break
}
}
+ public func fetchSelfLegalholdInfo() async throws -> TeamMemberLegalholdInfo {
+ let (selfUserID, _) = await teamLocalStore.selfUserInfo()
+
+ return try await teamsAPI.getLegalholdInfo(
+ for: selfTeamID,
+ userID: selfUserID
+ )
+ }
+
// MARK: - Private
private func fetchSelfTeamRemotely() async throws -> WireAPI.Team {
@@ -176,33 +227,6 @@ public class TeamRepository: TeamRepositoryProtocol {
}
}
- private func storeTeamLocally(_ teamAPIModel: WireAPI.Team) async {
- let selfUser = await userRepository.fetchSelfUser()
-
- await context.perform { [context] in
- let team = WireDataModel.Team.fetchOrCreate(
- with: teamAPIModel.id,
- in: context
- )
-
- _ = WireDataModel.Member.getOrUpdateMember(
- for: selfUser,
- in: team,
- context: context
- )
-
- team.name = teamAPIModel.name
- team.creator = ZMUser.fetchOrCreate(
- with: teamAPIModel.creatorID,
- domain: nil,
- in: context
- )
- team.pictureAssetId = teamAPIModel.logoID
- team.pictureAssetKey = teamAPIModel.logoKey
- team.needsToBeUpdatedFromBackend = false
- }
- }
-
private func fetchSelfTeamRolesRemotely() async throws -> [WireAPI.ConversationRole] {
do {
return try await teamsAPI.getTeamRoles(for: selfTeamID)
@@ -211,47 +235,6 @@ public class TeamRepository: TeamRepositoryProtocol {
}
}
- private func storeTeamRolesLocally(_ roles: [WireAPI.ConversationRole]) async throws {
- try await context.perform { [context, selfTeamID] in
- guard let team = WireDataModel.Team.fetch(
- with: selfTeamID,
- in: context
- ) else {
- throw TeamRepositoryError.teamNotFoundLocally
- }
-
- let existingRoles = team.roles
-
- let localRoles = roles.map { role in
- let localRole = Role.fetchOrCreate(
- name: role.name,
- teamOrConversation: .team(team),
- context: context
- )
-
- localRole.name = role.name
- localRole.team = team
-
- for action in role.actions {
- let action = Action.fetchOrCreate(
- name: action.name,
- in: context
- )
-
- localRole.actions.insert(action)
- }
-
- return localRole
- }
-
- for roleToDelete in existingRoles.subtracting(localRoles) {
- context.delete(roleToDelete)
- }
-
- team.needsToDownloadRoles = false
- }
- }
-
private func fetchSelfTeamMembersRemotely() async throws -> [WireAPI.TeamMember] {
do {
return try await teamsAPI.getTeamMembers(
@@ -263,71 +246,4 @@ public class TeamRepository: TeamRepositoryProtocol {
}
}
- private func storeTeamMembersLocally(_ teamMembers: [WireAPI.TeamMember]) async throws {
- try await context.perform { [context, selfTeamID] in
- guard let team = WireDataModel.Team.fetch(
- with: selfTeamID,
- in: context
- ) else {
- throw TeamRepositoryError.teamNotFoundLocally
- }
-
- for member in teamMembers {
- let user = ZMUser.fetchOrCreate(
- with: member.userID,
- domain: nil,
- in: context
- )
-
- let membership = Member.getOrUpdateMember(
- for: user,
- in: team,
- context: context
- )
-
- if let permissions = member.permissions {
- membership.permissions = Permissions(rawValue: permissions.selfPermissions)
- }
-
- if let creatorID = member.creatorID {
- membership.createdBy = ZMUser.fetchOrCreate(
- with: creatorID,
- domain: nil,
- in: context
- )
- }
-
- membership.createdAt = member.creationDate
- membership.needsToBeUpdatedFromBackend = false
- }
- }
- }
-
-}
-
-private extension ConversationAction {
-
- var name: String {
- switch self {
- case .addConversationMember:
- "add_conversation_member"
- case .removeConversationMember:
- "remove_conversation_member"
- case .modifyConversationName:
- "modify_conversation_name"
- case .modifyConversationMessageTimer:
- "modify_conversation_message_timer"
- case .modifyConversationReceiptMode:
- "modify_conversation_receipt_mode"
- case .modifyConversationAccess:
- "modify_conversation_access"
- case .modifyOtherConversationMember:
- "modify_other_conversation_member"
- case .leaveConversation:
- "leave_conversation"
- case .deleteConversation:
- "delete_conversation"
- }
- }
-
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepositoryError.swift b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepositoryError.swift
index 6b119bb7f20..48591d6f90a 100644
--- a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepositoryError.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepositoryError.swift
@@ -26,10 +26,6 @@ enum TeamRepositoryError: Error {
case failedToFetchRemotely(Error)
- /// The local team instance was not found in the database.
-
- case teamNotFoundLocally
-
/// User is not a member of the team.
case userNotAMemberInTeam(user: UUID)
diff --git a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsLocalStore.swift
new file mode 100644
index 00000000000..fbb13e0cd9d
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsLocalStore.swift
@@ -0,0 +1,165 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+import WireFoundation
+import WireLogging
+
+// sourcery: AutoMockable
+protocol UpdateEventsLocalStoreProtocol {
+
+ /// Get last event ID.
+ /// - returns: The last event ID.
+
+ func lastEventID() -> UUID?
+
+ /// Stores last event ID.
+ /// - parameter id: The last event ID to store.
+
+ func storeLastEventID(id: UUID)
+
+ /// Retrieves the index of the last event envelope.
+ /// - returns: The last index event envelope.
+
+ func indexOfLastEventEnvelope() async throws -> Int64
+
+ /// Persists an event envelope locally.
+ /// - Parameters:
+ /// - data: The event envelope payload data.
+ /// - index: The event envelope index.
+
+ func persistEventEnvelope(
+ _ data: Data,
+ index: Int64
+ ) async throws
+
+ /// Fetches stored event envelope payloads.
+ /// - parameter limit: A fetch limit.
+ /// - returns: A list of event payloads.
+
+ func fetchStoredEventEnvelopePayloads(
+ limit: UInt
+ ) async throws -> [Data]
+
+ /// Deletes next pending events locally.
+ /// - parameter limit: A fetch limit.
+
+ func deleteNextPendingEvents(
+ limit: UInt
+ ) async throws
+}
+
+final class UpdateEventsLocalStore: UpdateEventsLocalStoreProtocol {
+
+ enum Key: String, DefaultsKey {
+ case lastEventID
+ }
+
+ // MARK: - Error
+
+ enum Error: Swift.Error {
+ case failedToFetchStoredEvents(Swift.Error)
+ case failedToDeleteStoredEvents(Swift.Error)
+ }
+
+ // MARK: - Properties
+
+ private let context: NSManagedObjectContext
+ private let storage: PrivateUserDefaults
+
+ // MARK: - Object lifecycle
+
+ init(
+ context: NSManagedObjectContext,
+ userID: UUID,
+ sharedUserDefaults: UserDefaults
+ ) {
+ self.context = context
+ self.storage = PrivateUserDefaults(
+ userID: userID,
+ storage: sharedUserDefaults
+ )
+ }
+
+ // MARK: - Public
+
+ public func lastEventID() -> UUID? {
+ storage.getUUID(
+ forKey: .lastEventID
+ )
+ }
+
+ public func storeLastEventID(id: UUID) {
+ storage.setUUID(id, forKey: .lastEventID)
+ }
+
+ public func indexOfLastEventEnvelope() async throws -> Int64 {
+ try await context.perform { [context] in
+ let request = StoredUpdateEventEnvelope.sortedFetchRequest(asending: false)
+ request.fetchBatchSize = 1
+ let lastEnvelope = try context.fetch(request).first
+ return lastEnvelope?.sortIndex ?? 0
+ }
+ }
+
+ public func persistEventEnvelope(
+ _ data: Data,
+ index: Int64
+ ) async throws {
+ try await context.perform { [context] in
+ let storedEventEnvelope = StoredUpdateEventEnvelope(context: context)
+ storedEventEnvelope.data = data
+ storedEventEnvelope.sortIndex = index
+ try context.save()
+ }
+ }
+
+ public func fetchStoredEventEnvelopePayloads(
+ limit: UInt
+ ) async throws -> [Data] {
+ try await context.perform { [context] in
+ do {
+ let request = StoredUpdateEventEnvelope.sortedFetchRequest(asending: true)
+ request.fetchLimit = Int(limit)
+ request.returnsObjectsAsFaults = false
+ let storedEventEnvelopes = try context.fetch(request)
+ return storedEventEnvelopes.map(\.data)
+ } catch {
+ throw Error.failedToFetchStoredEvents(error)
+ }
+ }
+ }
+
+ public func deleteNextPendingEvents(
+ limit: UInt
+ ) async throws {
+ try await context.perform { [context] in
+ do {
+ let request = StoredUpdateEventEnvelope.sortedFetchRequest(asending: true)
+ request.fetchLimit = Int(limit)
+ let storedEventEnvelopes = try context.fetch(request)
+ WireLogger.sync.debug("deleting \(storedEventEnvelopes.count) stored envelopes")
+ storedEventEnvelopes.forEach(context.delete)
+ try context.save()
+ } catch {
+ throw Error.failedToDeleteStoredEvents(error)
+ }
+ }
+ }
+
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift
index bd6fe3c14e5..73ec07cf81e 100644
--- a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepository.swift
@@ -20,6 +20,7 @@ import Foundation
import WireAPI
import WireDataModel
import WireFoundation
+import WireLogging
// sourcery: AutoMockable
/// Access update events.
@@ -85,10 +86,6 @@ protocol UpdateEventsRepositoryProtocol {
final class UpdateEventsRepository: UpdateEventsRepositoryProtocol {
- enum Key: String, DefaultsKey {
- case lastEventID
- }
-
// MARK: - Properties
private let userID: UUID
@@ -96,11 +93,7 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol {
private let updateEventsAPI: any UpdateEventsAPI
private let pushChannel: any PushChannelProtocol
private let updateEventDecryptor: any UpdateEventDecryptorProtocol
- // swiftlint:disable:next todo_requires_jira_link
- // TODO: create UpdateEventsLocalStore
- private let eventContext: NSManagedObjectContext
- private let storage: PrivateUserDefaults
-
+ private let updateEventsLocalStore: any UpdateEventsLocalStoreProtocol
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
@@ -112,19 +105,14 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol {
updateEventsAPI: any UpdateEventsAPI,
pushChannel: any PushChannelProtocol,
updateEventDecryptor: any UpdateEventDecryptorProtocol,
- eventContext: NSManagedObjectContext,
- sharedUserDefaults: UserDefaults
+ updateEventsLocalStore: any UpdateEventsLocalStoreProtocol
) {
self.userID = userID
self.selfClientID = selfClientID
self.updateEventsAPI = updateEventsAPI
self.pushChannel = pushChannel
self.updateEventDecryptor = updateEventDecryptor
- self.eventContext = eventContext
- self.storage = PrivateUserDefaults(
- userID: userID,
- storage: sharedUserDefaults
- )
+ self.updateEventsLocalStore = updateEventsLocalStore
}
// MARK: - Pull pending events
@@ -132,12 +120,12 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol {
func pullPendingEvents() async throws {
WireLogger.sync.debug("pulling pending events")
// We want all events since this event.
- guard let lastEventID = storage.getUUID(forKey: .lastEventID) else {
+ guard let lastEventID = updateEventsLocalStore.lastEventID() else {
throw UpdateEventsRepositoryError.lastEventIDMissing
}
// We'll insert new events from this index.
- var currentIndex = try await indexOfLastEventEnvelope() + 1
+ var currentIndex = try await updateEventsLocalStore.indexOfLastEventEnvelope() + 1
// Events are fetched in batches.
for try await envelopes in updateEventsAPI.getUpdateEvents(
@@ -168,8 +156,10 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol {
attributes: [.eventEnvelopeID: envelope.id]
)
- try await persistEventEnvelope(
- decryptedEnvelope,
+ let decryptedEnvelopeData = try encoder.encode(decryptedEnvelope)
+
+ try await updateEventsLocalStore.persistEventEnvelope(
+ decryptedEnvelopeData,
index: currentIndex
)
@@ -192,49 +182,13 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol {
storeLastEventEnvelopeID(lastEvent.id)
}
- private func indexOfLastEventEnvelope() async throws -> Int64 {
- try await eventContext.perform { [eventContext] in
- let request = StoredUpdateEventEnvelope.sortedFetchRequest(asending: false)
- request.fetchBatchSize = 1
- let lastEnvelope = try eventContext.fetch(request).first
- return lastEnvelope?.sortIndex ?? 0
- }
- }
-
- private func persistEventEnvelope(
- _ eventEnvelope: UpdateEventEnvelope,
- index: Int64
- ) async throws {
- try await eventContext.perform { [eventContext, encoder] in
- let data = try encoder.encode(eventEnvelope)
- let storedEventEnvelope = StoredUpdateEventEnvelope(context: eventContext)
- storedEventEnvelope.data = data
- storedEventEnvelope.sortIndex = index
- try eventContext.save()
- }
- }
-
// MARK: - Fetch pending events
func fetchNextPendingEvents(limit: UInt) async throws -> [UpdateEventEnvelope] {
- let payloads = try await fetchStoredEventEnvelopePayloads(limit: limit)
+ let payloads = try await updateEventsLocalStore.fetchStoredEventEnvelopePayloads(limit: limit)
return try decodeEventEnvelopes(payloads)
}
- private func fetchStoredEventEnvelopePayloads(limit: UInt) async throws -> [Data] {
- try await eventContext.perform { [eventContext] in
- do {
- let request = StoredUpdateEventEnvelope.sortedFetchRequest(asending: true)
- request.fetchLimit = Int(limit)
- request.returnsObjectsAsFaults = false
- let storedEventEnvelopes = try eventContext.fetch(request)
- return storedEventEnvelopes.map(\.data)
- } catch {
- throw UpdateEventsRepositoryError.failedToFetchStoredEvents(error)
- }
- }
- }
-
private func decodeEventEnvelopes(_ payloads: [Data]) throws -> [UpdateEventEnvelope] {
try payloads.map {
do {
@@ -248,18 +202,7 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol {
// MARK: - Delete pending events
func deleteNextPendingEvents(limit: UInt) async throws {
- try await eventContext.perform { [eventContext] in
- do {
- let request = StoredUpdateEventEnvelope.sortedFetchRequest(asending: true)
- request.fetchLimit = Int(limit)
- let storedEventEnvelopes = try eventContext.fetch(request)
- WireLogger.sync.debug("deleting \(storedEventEnvelopes.count) stored envelopes")
- storedEventEnvelopes.forEach(eventContext.delete)
- try eventContext.save()
- } catch {
- throw UpdateEventsRepositoryError.failedToDeleteStoredEvents(error)
- }
- }
+ try await updateEventsLocalStore.deleteNextPendingEvents(limit: limit)
}
// MARK: - Live events
@@ -294,7 +237,7 @@ final class UpdateEventsRepository: UpdateEventsRepositoryProtocol {
attributes: [.eventEnvelopeID: id]
)
- storage.setUUID(id, forKey: .lastEventID)
+ updateEventsLocalStore.storeLastEventID(id: id)
}
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepositoryError.swift b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepositoryError.swift
index 4d8703798e6..68828db00f3 100644
--- a/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepositoryError.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/UpdateEvents/UpdateEventsRepositoryError.swift
@@ -21,8 +21,6 @@ import Foundation
enum UpdateEventsRepositoryError: Error {
case lastEventIDMissing
- case failedToFetchStoredEvents(Error)
case failedToDecodeStoredEvent(Error)
- case failedToDeleteStoredEvents(Error)
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/User/Models/NewUserInfo.swift b/WireDomain/Sources/WireDomain/Repositories/User/Models/NewUserInfo.swift
new file mode 100644
index 00000000000..33cdb554efa
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/User/Models/NewUserInfo.swift
@@ -0,0 +1,35 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+
+public struct NewUserInfo: Sendable {
+ let userID: WireDataModel.QualifiedID
+ let name: String
+ let handle: String?
+ let teamID: UUID?
+ let accentID: Int
+ let previewAssetKey: String?
+ let completeAssetKey: String?
+ let deleted: Bool?
+ let email: String?
+ let expiresAt: Date?
+ let serviceID: UUID?
+ let serviceProvider: UUID?
+ let supportedProtocols: Set?
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/User/Models/UserUpdateInfo.swift b/WireDomain/Sources/WireDomain/Repositories/User/Models/UserUpdateInfo.swift
new file mode 100644
index 00000000000..c405eae35c4
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/User/Models/UserUpdateInfo.swift
@@ -0,0 +1,31 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+
+public struct UserUpdateInfo: Sendable {
+ let userID: UUID
+ let accentColorID: Int?
+ let name: String?
+ let handle: String?
+ let email: String?
+ let isSSOIDDeleted: Bool?
+ let previewAssetKey: String?
+ let completeAssetKey: String?
+ let supportedProtocols: Set?
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift
index e209b17ad69..3ecf82ca186 100644
--- a/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift
@@ -16,9 +16,8 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import CoreData
-import WireAPI
import WireDataModel
+import WireLogging
// sourcery: AutoMockable
/// A local store dedicated to user.
@@ -52,32 +51,19 @@ public protocol UserLocalStoreProtocol {
domain: String?
) async -> ZMUser
- /// Removes user push token from storage.
-
- func deletePushToken()
-
- /// Fetches or creates a user client locally.
+ /// Fetches or creates users locally.
///
/// - parameters:
- /// - id: The user client id to find or create locally.
- /// - returns: The user client found or created locally and a flag indicating whether or not the user client is new.
+ /// - userIDs: The users id to fetch or create locally.
+ /// - returns: A list of users fetched or created locally.
- func fetchOrCreateUserClient(
- id: String
- ) async -> (client: WireDataModel.UserClient, isNew: Bool)
+ func fetchOrCreateUsers(
+ userIDs: [(id: UUID, domain: String?)]
+ ) async -> Set
- /// Updates the user client informations locally.
- ///
- /// - parameters:
- /// - localClient: The user client to update locally.
- /// - remoteClient: The up-to-date remote user client.
- /// - isNewClient: A flag indicating whether the user client is new.
+ /// Removes user push token from storage.
- func updateUserClient(
- _ localClient: WireDataModel.UserClient,
- from remoteClient: WireAPI.SelfUserClient,
- isNewClient: Bool
- ) async throws
+ func deletePushToken()
/// Adds a legal hold request to self.
///
@@ -133,9 +119,14 @@ public protocol UserLocalStoreProtocol {
func markAccountAsDeleted(for user: ZMUser) async
- // TODO: [WPB-10727] Merge these two methods into a single method (also no API objects should be passed to local store)
- func persistUser(from user: WireAPI.User) async
- func updateUser(from event: UserUpdateEvent) async
+ // TODO: [WPB-10727] Merge these two methods into a single method
+ func persistUser(userInfo: NewUserInfo) async
+ func updateUser(userUpdateInfo: UserUpdateInfo) async
+
+ /// Fetches all user IDs that have a one on one conversation
+ /// - returns: A list of users' qualified IDs.
+
+ func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID]
}
public final class UserLocalStore: UserLocalStoreProtocol {
@@ -195,6 +186,26 @@ public final class UserLocalStore: UserLocalStoreProtocol {
}
}
+ public func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] {
+ try await context.perform { [context] in
+ let request = NSFetchRequest(entityName: ZMUser.entityName())
+ let predicate = NSPredicate(format: "%K != nil", #keyPath(ZMUser.oneOnOneConversation))
+ request.predicate = predicate
+
+ return try context
+ .fetch(request)
+ .compactMap { user in
+ guard let userID = user.qualifiedID else {
+ WireLogger.conversation.error(
+ "Missing user's qualifiedID"
+ )
+ return nil
+ }
+ return userID
+ }
+ }
+ }
+
public func fetchUsersQualifiedIDs() async throws -> [WireDataModel.QualifiedID] {
try await context.perform {
let fetchRequest = NSFetchRequest(entityName: ZMUser.entityName())
@@ -254,20 +265,21 @@ public final class UserLocalStore: UserLocalStoreProtocol {
}
}
- public func fetchOrCreateUserClient(
- id: String
- ) async -> (client: WireDataModel.UserClient, isNew: Bool) {
+ public func fetchOrCreateUsers(
+ userIDs: [(id: UUID, domain: String?)]
+ ) async -> Set {
+
await context.perform { [context] in
- if let existingClient = UserClient.fetchExistingUserClient(
- with: id,
- in: context
- ) {
- return (existingClient, false)
- } else {
- let newClient = UserClient.insertNewObject(in: context)
- newClient.remoteIdentifier = id
- return (newClient, true)
+
+ let users = userIDs.map {
+ ZMUser.fetchOrCreate(
+ with: $0.id,
+ domain: $0.domain,
+ in: context
+ )
}
+
+ return Set(users)
}
}
@@ -290,54 +302,52 @@ public final class UserLocalStore: UserLocalStoreProtocol {
}
}
- // swiftlint:disable:next todo_requires_jira_link
- // TODO: refactor, do not pass API object (WireAPI.UserClient) directly, merge this method with updateUser method.
- public func persistUser(from user: WireAPI.User) async {
+ public func persistUser(userInfo: NewUserInfo) async {
let persistedUser = await fetchOrCreateUser(
- id: user.id.uuid,
- domain: user.id.domain
+ id: userInfo.userID.uuid,
+ domain: userInfo.userID.domain
)
await context.perform {
- guard user.deleted == false else {
+ guard userInfo.deleted == false else {
return persistedUser.markAccountAsDeleted(at: Date())
}
- persistedUser.name = user.name
- persistedUser.handle = user.handle
- persistedUser.teamIdentifier = user.teamID
- persistedUser.accentColorValue = Int16(user.accentID)
- persistedUser.previewProfileAssetIdentifier = user.assets.first(where: { $0.size == .preview })?.key
- persistedUser.previewProfileAssetIdentifier = user.assets.first(where: { $0.size == .complete })?.key
- persistedUser.emailAddress = user.email
- persistedUser.expiresAt = user.expiresAt
- persistedUser.serviceIdentifier = user.service?.id.transportString()
- persistedUser.providerIdentifier = user.service?.provider.transportString()
- persistedUser.supportedProtocols = user.supportedProtocols?.toDomainModel() ?? [.proteus]
+ persistedUser.name = userInfo.name
+ persistedUser.handle = userInfo.handle
+ persistedUser.teamIdentifier = userInfo.teamID
+ persistedUser.accentColorValue = Int16(userInfo.accentID)
+ persistedUser.previewProfileAssetIdentifier = userInfo.previewAssetKey
+ persistedUser.previewProfileAssetIdentifier = userInfo.completeAssetKey
+ persistedUser.emailAddress = userInfo.email
+ persistedUser.expiresAt = userInfo.expiresAt
+ persistedUser.serviceIdentifier = userInfo.serviceID?.transportString()
+ persistedUser.providerIdentifier = userInfo.serviceProvider?.transportString()
+ persistedUser.supportedProtocols = userInfo.supportedProtocols ?? [.proteus]
persistedUser.needsToBeUpdatedFromBackend = false
}
}
// TODO: [WPB-10727] reuse `updateUserMetadata` from mentioned ticket's implementation to avoid code duplication
- public func updateUser(from event: UserUpdateEvent) async {
+ public func updateUser(userUpdateInfo: UserUpdateInfo) async {
let user = await fetchOrCreateUser(
- id: event.userID
+ id: userUpdateInfo.userID
)
await context.perform {
- if let name = event.name {
+ if let name = userUpdateInfo.name {
user.name = name
}
- if let email = event.email {
+ if let email = userUpdateInfo.email {
user.emailAddress = email
}
- if let handle = event.handle {
+ if let handle = userUpdateInfo.handle {
user.handle = handle
}
- if let accentColor = event.accentColorID {
+ if let accentColor = userUpdateInfo.accentColorID {
user.accentColorValue = Int16(accentColor)
}
@@ -350,13 +360,9 @@ public final class UserLocalStore: UserLocalStoreProtocol {
/// changes to its assets
/// we don't want to update them and keep these changes as is until they're synced.
if !user.hasLocalModifications(forKeys: assetKeys) {
- let previewAssetKey = event.assets?
- .first(where: { $0.size == .preview })
- .map(\.key)
+ let previewAssetKey = userUpdateInfo.previewAssetKey
- let completeAssetKey = event.assets?
- .first(where: { $0.size == .complete })
- .map(\.key)
+ let completeAssetKey = userUpdateInfo.completeAssetKey
if let previewAssetKey {
user.previewProfileAssetIdentifier = previewAssetKey
@@ -367,68 +373,9 @@ public final class UserLocalStore: UserLocalStoreProtocol {
}
}
- user.supportedProtocols = event.supportedProtocols?.toDomainModel() ?? [.proteus]
+ user.supportedProtocols = userUpdateInfo.supportedProtocols ?? [.proteus]
user.isPendingMetadataRefresh = false
}
}
-
- // swiftlint:disable:next todo_requires_jira_link
- // TODO: refactor, do not pass API object (WireAPI.UserClient) directly
- public func updateUserClient(
- _ localClient: WireDataModel.UserClient,
- from remoteClient: WireAPI.SelfUserClient,
- isNewClient: Bool
- ) async throws {
- await context.perform { [context] in
-
- localClient.label = remoteClient.label
- localClient.type = remoteClient.type.toDomainModel()
- localClient.model = remoteClient.model
- localClient.deviceClass = remoteClient.deviceClass?.toDomainModel()
- localClient.activationDate = remoteClient.activationDate
- localClient.lastActiveDate = remoteClient.lastActiveDate
- localClient.remoteIdentifier = remoteClient.id
-
- let selfUser = ZMUser.selfUser(in: context)
- localClient.user = localClient.user ?? selfUser
-
- if isNewClient {
- localClient.needsSessionMigration = selfUser.domain == nil
- }
-
- if localClient.isLegalHoldDevice, isNewClient {
- selfUser.legalHoldRequest = nil
- selfUser.needsToAcknowledgeLegalHoldStatus = true
- }
-
- if !localClient.isSelfClient() {
- localClient.mlsPublicKeys = .init(
- ed25519: remoteClient.mlsPublicKeys?.ed25519,
- ed448: remoteClient.mlsPublicKeys?.ed448,
- p256: remoteClient.mlsPublicKeys?.p256,
- p384: remoteClient.mlsPublicKeys?.p384,
- p521: remoteClient.mlsPublicKeys?.p512
- )
- }
-
- let selfClient = selfUser.selfClient()
- let isNotSameId = localClient.remoteIdentifier != selfClient?.remoteIdentifier
- let localClientActivationDate = localClient.activationDate
- let selfClientActivationDate = selfClient?.activationDate
-
- if selfClient != nil, isNotSameId, let localClientActivationDate, let selfClientActivationDate {
- let comparisonResult = localClientActivationDate
- .compare(selfClientActivationDate)
-
- if comparisonResult == .orderedDescending {
- localClient.needsToNotifyUser = true
- }
- }
-
- selfUser.selfClient()?.addNewClientToIgnored(localClient)
- selfUser.selfClient()?.updateSecurityLevelAfterDiscovering(Set([localClient]))
- }
- }
-
}
diff --git a/WireDomain/Sources/WireDomain/Repositories/User/UserModelMappings.swift b/WireDomain/Sources/WireDomain/Repositories/User/UserModelMappings.swift
index 5e1f11e7ac3..b2e5da40fd8 100644
--- a/WireDomain/Sources/WireDomain/Repositories/User/UserModelMappings.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/User/UserModelMappings.swift
@@ -103,3 +103,79 @@ extension WireAPI.Prekey {
}
}
+
+extension WireAPI.UserUpdateEvent {
+
+ func toDomainModel() -> UserUpdateInfo {
+ .init(
+ userID: userID,
+ accentColorID: accentColorID,
+ name: name,
+ handle: handle,
+ email: email,
+ isSSOIDDeleted: isSSOIDDeleted,
+ previewAssetKey: assets?
+ .first(where: { $0.size == .preview })
+ .map(\.key),
+ completeAssetKey: assets?
+ .first(where: { $0.size == .complete })
+ .map(\.key),
+ supportedProtocols: supportedProtocols?.toDomainModel()
+ )
+ }
+
+}
+
+extension WireAPI.User {
+
+ func toDomainModel() -> NewUserInfo {
+
+ .init(
+ userID: id.toDomainModel(),
+ name: name,
+ handle: handle,
+ teamID: teamID,
+ accentID: accentID,
+ previewAssetKey: assets
+ .first(where: { $0.size == .preview })
+ .map(\.key),
+ completeAssetKey: assets
+ .first(where: { $0.size == .complete })
+ .map(\.key),
+ deleted: deleted,
+ email: email,
+ expiresAt: expiresAt,
+ serviceID: service?.id,
+ serviceProvider: service?.provider,
+ supportedProtocols: supportedProtocols?.toDomainModel()
+ )
+
+ }
+
+}
+
+extension WireAPI.SelfUser {
+
+ func toDomainModel() -> NewUserInfo {
+ .init(
+ userID: qualifiedID.toDomainModel(),
+ name: name,
+ handle: handle,
+ teamID: teamID,
+ accentID: accentID,
+ previewAssetKey: assets?
+ .first(where: { $0.size == .preview })
+ .map(\.key),
+ completeAssetKey: assets?
+ .first(where: { $0.size == .complete })
+ .map(\.key),
+ deleted: deleted,
+ email: email,
+ expiresAt: expiresAt,
+ serviceID: service?.id,
+ serviceProvider: service?.provider,
+ supportedProtocols: supportedProtocols?.toDomainModel()
+ )
+ }
+
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift b/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift
index a4823583947..2a1427d61f6 100644
--- a/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift
@@ -20,6 +20,7 @@ import Foundation
import WireAPI
import WireDataModel
import WireFoundation
+import WireLogging
// sourcery: AutoMockable
/// Facilitate access to users related domain objects.
@@ -29,6 +30,10 @@ import WireFoundation
/// as well as the possible source(s) of the models.
public protocol UserRepositoryProtocol {
+ /// Pulls self user and stores it locally
+
+ func pullSelfUser() async throws
+
/// Fetch self user from the local store
func fetchSelfUser() async -> ZMUser
@@ -149,6 +154,11 @@ public protocol UserRepositoryProtocol {
domain: String?
) async throws -> Bool
+ /// Fetches all user IDs that have a one on one conversation
+ /// - returns: A list of users' qualified IDs.
+
+ func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID]
+
}
public final class UserRepository: UserRepositoryProtocol {
@@ -179,6 +189,14 @@ public final class UserRepository: UserRepositoryProtocol {
// MARK: - Public
+ public func pullSelfUser() async throws {
+ let selfUser = try await selfUserAPI.getSelfUser()
+
+ await userLocalStore.persistUser(
+ userInfo: selfUser.toDomainModel()
+ )
+ }
+
public func fetchSelfUser() async -> ZMUser {
await userLocalStore.fetchSelfUser()
}
@@ -226,7 +244,7 @@ public final class UserRepository: UserRepositoryProtocol {
let userList = try await usersAPI.getUsers(userIDs: userIDs.toAPIModel())
for user in userList.found {
- await userLocalStore.persistUser(from: user)
+ await userLocalStore.persistUser(userInfo: user.toDomainModel())
}
} catch {
@@ -238,7 +256,7 @@ public final class UserRepository: UserRepositoryProtocol {
from event: UserUpdateEvent
) async {
await userLocalStore.updateUser(
- from: event
+ userUpdateInfo: event.toDomainModel()
)
}
@@ -333,6 +351,10 @@ public final class UserRepository: UserRepositoryProtocol {
}
}
+ public func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] {
+ try await userLocalStore.fetchAllUserIDsWithOneOnOneConversation()
+ }
+
public func isSelfUser(
id: UUID,
domain: String?
diff --git a/WireDomain/Sources/WireDomain/Repositories/UserClients/Models/UserClientInfo.swift b/WireDomain/Sources/WireDomain/Repositories/UserClients/Models/UserClientInfo.swift
new file mode 100644
index 00000000000..f0544d2f714
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/UserClients/Models/UserClientInfo.swift
@@ -0,0 +1,40 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+
+public struct UserClientInfo: Sendable {
+
+ let id: String
+ let label: String?
+ let type: WireDataModel.DeviceType
+ let activationDate: Date
+ let model: String?
+ let deviceClass: WireDataModel.DeviceClass?
+ let lastActiveDate: Date?
+ let mlsPublicKeys: UserClientInfo.MLSPublicKeys?
+
+ struct MLSPublicKeys {
+ let ed25519: String?
+ let ed448: String?
+ let p256: String?
+ let p384: String?
+ let p512: String?
+ }
+
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/UserClients/UserClientsLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/UserClients/UserClientsLocalStore.swift
new file mode 100644
index 00000000000..8ff2478db73
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/UserClients/UserClientsLocalStore.swift
@@ -0,0 +1,229 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+import WireLogging
+
+// sourcery: AutoMockable
+public protocol UserClientsLocalStoreProtocol {
+
+ /// Fetches or creates a client locally.
+ ///
+ /// - parameters:
+ /// - id: The user client id to find or create locally.
+ /// - returns: The user client found or created locally and a flag indicating whether or not the user client is new.
+
+ func fetchOrCreateClient(
+ id: String
+ ) async -> (client: WireDataModel.UserClient, isNew: Bool)
+
+ /// Retrieves deleted self clients locally based on new self clients.
+ /// - parameter newClients: The new self user clients.
+ /// - returns: A list of deleted self clients.
+
+ func deletedSelfClients(
+ newClients: [String]
+ ) async -> [String]
+
+ /// Deletes client locally.
+ /// - parameter id: The client id.
+
+ func deleteClient(
+ id: String
+ ) async
+
+ /// Updates the user client informations locally.
+ ///
+ /// - parameters:
+ /// - id: The user client id.
+ /// - isNewClient: A flag indicating whether the user client is new.
+ /// - remoteClient: The up-to-date user client info object.
+
+ func updateClient(
+ id: String,
+ isNewClient: Bool,
+ userClientInfo: UserClientInfo
+ ) async
+
+ /// Indicates whether self user clients are active MLS clients.
+ /// - returns: A flag indicating whether all self user clients are active MLS clients.
+
+ func allSelfUserClientsAreActiveMLSClients() async -> Bool
+}
+
+public final class UserClientsLocalStore: UserClientsLocalStoreProtocol {
+
+ // MARK: - Properties
+
+ private let context: NSManagedObjectContext
+ private let userLocalStore: any UserLocalStoreProtocol
+
+ // MARK: - Object lifecycle
+
+ init(
+ context: NSManagedObjectContext,
+ userLocalStore: any UserLocalStoreProtocol
+ ) {
+ self.context = context
+ self.userLocalStore = userLocalStore
+ }
+
+ public func fetchOrCreateClient(
+ id: String
+ ) async -> (client: WireDataModel.UserClient, isNew: Bool) {
+ await context.perform { [context] in
+ if let existingClient = UserClient.fetchExistingUserClient(
+ with: id,
+ in: context
+ ) {
+ return (existingClient, false)
+ } else {
+ let newClient = UserClient.insertNewObject(in: context)
+ newClient.remoteIdentifier = id
+ return (newClient, true)
+ }
+ }
+ }
+
+ public func deletedSelfClients(
+ newClients: [String]
+ ) async -> [String] {
+ let selfUser = await userLocalStore.fetchSelfUser()
+
+ return await context.perform {
+ selfUser.clients
+ .compactMap(\.remoteIdentifier)
+ .filter {
+ !newClients.contains($0)
+ }
+ }
+ }
+
+ public func deleteClient(
+ id: String
+ ) async {
+ let localClient = await context.perform { [context] in
+ return UserClient.fetchExistingUserClient(
+ with: id,
+ in: context
+ )
+ }
+
+ guard let localClient else {
+ return WireLogger.userClient.error(
+ "Failed to find existing client with id: \(id.redactedAndTruncated())"
+ )
+ }
+
+ await localClient.deleteClientAndEndSession()
+ }
+
+ public func updateClient(
+ id: String,
+ isNewClient: Bool,
+ userClientInfo: UserClientInfo
+ ) async {
+ await context.perform { [context] in
+
+ guard let localClient = UserClient.fetchExistingUserClient(
+ with: id,
+ in: context
+ ) else {
+ return WireLogger.userClient.error(
+ "Failed to find existing client with id: \(id.redactedAndTruncated())"
+ )
+ }
+
+ localClient.label = userClientInfo.label
+ localClient.type = userClientInfo.type
+ localClient.model = userClientInfo.model
+ localClient.deviceClass = userClientInfo.deviceClass
+ localClient.activationDate = userClientInfo.activationDate
+ localClient.lastActiveDate = userClientInfo.lastActiveDate
+ localClient.remoteIdentifier = userClientInfo.id
+
+ let selfUser = ZMUser.selfUser(in: context)
+ localClient.user = localClient.user ?? selfUser
+
+ if isNewClient {
+ localClient.needsSessionMigration = selfUser.domain == nil
+ }
+
+ if localClient.isLegalHoldDevice, isNewClient {
+ selfUser.legalHoldRequest = nil
+ selfUser.needsToAcknowledgeLegalHoldStatus = true
+ }
+
+ if !localClient.isSelfClient() {
+ localClient.mlsPublicKeys = .init(
+ ed25519: userClientInfo.mlsPublicKeys?.ed25519,
+ ed448: userClientInfo.mlsPublicKeys?.ed448,
+ p256: userClientInfo.mlsPublicKeys?.p256,
+ p384: userClientInfo.mlsPublicKeys?.p384,
+ p521: userClientInfo.mlsPublicKeys?.p512
+ )
+ }
+
+ let selfClient = selfUser.selfClient()
+ let isNotSameId = localClient.remoteIdentifier != selfClient?.remoteIdentifier
+ let localClientActivationDate = localClient.activationDate
+ let selfClientActivationDate = selfClient?.activationDate
+
+ if selfClient != nil, isNotSameId, let localClientActivationDate, let selfClientActivationDate {
+ let comparisonResult = localClientActivationDate
+ .compare(selfClientActivationDate)
+
+ if comparisonResult == .orderedDescending {
+ localClient.needsToNotifyUser = true
+ }
+ }
+
+ selfUser.selfClient()?.addNewClientToIgnored(localClient)
+ selfUser.selfClient()?.updateSecurityLevelAfterDiscovering(Set([localClient]))
+ }
+ }
+
+ public func allSelfUserClientsAreActiveMLSClients() async -> Bool {
+ let selfUser = await userLocalStore.fetchSelfUser()
+
+ return await context.perform {
+ selfUser.clients.all { userClient in
+ let hasMLSIdentity = !userClient.mlsPublicKeys.isEmpty
+
+ let isRecentlyActive: Bool = {
+ if userClient.isSelfClient() {
+ return true
+ }
+
+ guard let lastActiveDate = userClient.lastActiveDate else {
+ return false
+ }
+
+ guard lastActiveDate <= Date() else {
+ return true
+ }
+
+ return lastActiveDate.timeIntervalSinceNow.magnitude < .fourWeeks
+ }()
+
+ return hasMLSIdentity && isRecentlyActive
+ }
+ }
+ }
+
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/UserClients/UserClientsModelMappings.swift b/WireDomain/Sources/WireDomain/Repositories/UserClients/UserClientsModelMappings.swift
new file mode 100644
index 00000000000..23c0c5f77f7
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/Repositories/UserClients/UserClientsModelMappings.swift
@@ -0,0 +1,42 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireAPI
+
+extension WireAPI.SelfUserClient {
+
+ func toDomainModel() -> UserClientInfo {
+ .init(
+ id: id,
+ label: label,
+ type: type.toDomainModel(),
+ activationDate: activationDate,
+ model: model,
+ deviceClass: deviceClass?.toDomainModel(),
+ lastActiveDate: lastActiveDate,
+ mlsPublicKeys: .init(
+ ed25519: mlsPublicKeys?.ed25519,
+ ed448: mlsPublicKeys?.ed448,
+ p256: mlsPublicKeys?.p256,
+ p384: mlsPublicKeys?.p384,
+ p512: mlsPublicKeys?.p512
+ )
+ )
+ }
+
+}
diff --git a/WireDomain/Sources/WireDomain/Repositories/UserClients/UserClientsRepository.swift b/WireDomain/Sources/WireDomain/Repositories/UserClients/UserClientsRepository.swift
index 02309b8bec6..a2435614519 100644
--- a/WireDomain/Sources/WireDomain/Repositories/UserClients/UserClientsRepository.swift
+++ b/WireDomain/Sources/WireDomain/Repositories/UserClients/UserClientsRepository.swift
@@ -16,7 +16,6 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import CoreData
import Foundation
import WireAPI
import WireDataModel
@@ -42,7 +41,7 @@ public protocol UserClientsRepositoryProtocol {
/// - returns: The user client found or created locally and a flag indicating whether or not the user client is new.
func fetchOrCreateClient(
- with id: String
+ id: String
) async throws -> (client: WireDataModel.UserClient, isNew: Bool)
/// Updates the user client informations locally.
@@ -53,7 +52,7 @@ public protocol UserClientsRepositoryProtocol {
/// - isNewClient: A flag indicating whether the user client is new.
func updateClient(
- with id: String,
+ id: String,
from remoteClient: WireAPI.SelfUserClient,
isNewClient: Bool
) async throws
@@ -61,7 +60,7 @@ public protocol UserClientsRepositoryProtocol {
/// Deletes client locally.
/// - parameter id: The client id.
- func deleteClient(with id: String) async
+ func deleteClient(id: String) async
/// Indicates whether self user clients are active MLS clients.
/// - returns: A flag indicating whether all self user clients are active MLS clients.
@@ -75,175 +74,73 @@ public struct UserClientsRepository: UserClientsRepositoryProtocol {
private let userClientsAPI: any UserClientsAPI
private let userRepository: any UserRepositoryProtocol
- private let context: NSManagedObjectContext
+ private let userClientsLocalStore: any UserClientsLocalStoreProtocol
// MARK: - Object lifecycle
init(
userClientsAPI: any UserClientsAPI,
userRepository: any UserRepositoryProtocol,
- context: NSManagedObjectContext
+ userClientsLocalStore: any UserClientsLocalStoreProtocol
) {
self.userClientsAPI = userClientsAPI
self.userRepository = userRepository
- self.context = context
+ self.userClientsLocalStore = userClientsLocalStore
}
// MARK: - Public
+ public func fetchOrCreateClient(
+ id: String
+ ) async throws -> (client: WireDataModel.UserClient, isNew: Bool) {
+ await userClientsLocalStore.fetchOrCreateClient(
+ id: id
+ )
+ }
+
+ public func deleteClient(
+ id: String
+ ) async {
+ await userClientsLocalStore.deleteClient(id: id)
+ }
+
public func pullSelfClients() async throws {
let remoteSelfClients = try await userClientsAPI.getSelfClients()
- let selfUser = await userRepository.fetchSelfUser()
- let localSelfClients = await context.perform {
- selfUser.clients
- }
for remoteSelfClient in remoteSelfClients {
- let localUserClient = try await fetchOrCreateClient(with: remoteSelfClient.id)
+ let localUserClient = await userClientsLocalStore.fetchOrCreateClient(
+ id: remoteSelfClient.id
+ )
+
try await updateClient(
- with: remoteSelfClient.id,
+ id: remoteSelfClient.id,
from: remoteSelfClient,
isNewClient: localUserClient.isNew
)
}
- let deletedSelfClientsIDs = await context.perform {
- localSelfClients
- .compactMap(\.remoteIdentifier)
- .filter {
- !remoteSelfClients.map(\.id).contains($0)
- }
- }
+ let deletedSelfClientsIDs = await userClientsLocalStore.deletedSelfClients(
+ newClients: remoteSelfClients.map(\.id)
+ )
for deletedSelfClientID in deletedSelfClientsIDs {
- await deleteClient(with: deletedSelfClientID)
- }
- }
-
- public func fetchOrCreateClient(
- with id: String
- ) async throws -> (client: WireDataModel.UserClient, isNew: Bool) {
- await context.perform { [context] in
- if let existingClient = UserClient.fetchExistingUserClient(
- with: id,
- in: context
- ) {
- return (existingClient, false)
- } else {
- let newClient = UserClient.insertNewObject(in: context)
- newClient.remoteIdentifier = id
- return (newClient, true)
- }
+ await userClientsLocalStore.deleteClient(id: deletedSelfClientID)
}
}
public func updateClient(
- with id: String,
+ id: String,
from remoteClient: WireAPI.SelfUserClient,
isNewClient: Bool
) async throws {
- await context.perform { [context] in
-
- guard let localClient = UserClient.fetchExistingUserClient(
- with: id,
- in: context
- ) else {
- return WireLogger.userClient.error(
- "Failed to find existing client with id: \(id.redactedAndTruncated())"
- )
- }
-
- localClient.label = remoteClient.label
- localClient.type = remoteClient.type.toDomainModel()
- localClient.model = remoteClient.model
- localClient.deviceClass = remoteClient.deviceClass?.toDomainModel()
- localClient.activationDate = remoteClient.activationDate
- localClient.lastActiveDate = remoteClient.lastActiveDate
- localClient.remoteIdentifier = remoteClient.id
-
- let selfUser = ZMUser.selfUser(in: context)
- localClient.user = localClient.user ?? selfUser
-
- if isNewClient {
- localClient.needsSessionMigration = selfUser.domain == nil
- }
-
- if localClient.isLegalHoldDevice, isNewClient {
- selfUser.legalHoldRequest = nil
- selfUser.needsToAcknowledgeLegalHoldStatus = true
- }
-
- if !localClient.isSelfClient() {
- localClient.mlsPublicKeys = .init(
- ed25519: remoteClient.mlsPublicKeys?.ed25519,
- ed448: remoteClient.mlsPublicKeys?.ed448,
- p256: remoteClient.mlsPublicKeys?.p256,
- p384: remoteClient.mlsPublicKeys?.p384,
- p521: remoteClient.mlsPublicKeys?.p512
- )
- }
-
- let selfClient = selfUser.selfClient()
- let isNotSameId = localClient.remoteIdentifier != selfClient?.remoteIdentifier
- let localClientActivationDate = localClient.activationDate
- let selfClientActivationDate = selfClient?.activationDate
-
- if selfClient != nil, isNotSameId, let localClientActivationDate, let selfClientActivationDate {
- let comparisonResult = localClientActivationDate
- .compare(selfClientActivationDate)
-
- if comparisonResult == .orderedDescending {
- localClient.needsToNotifyUser = true
- }
- }
-
- selfUser.selfClient()?.addNewClientToIgnored(localClient)
- selfUser.selfClient()?.updateSecurityLevelAfterDiscovering(Set([localClient]))
- }
- }
-
- public func deleteClient(with id: String) async {
- let localClient = await context.perform {
- UserClient.fetchExistingUserClient(
- with: id,
- in: context
- )
- }
-
- guard let localClient else {
- return WireLogger.userClient.error(
- "Failed to find existing client with id: \(id.redactedAndTruncated())"
- )
- }
-
- await localClient.deleteClientAndEndSession()
+ await userClientsLocalStore.updateClient(
+ id: id,
+ isNewClient: isNewClient,
+ userClientInfo: remoteClient.toDomainModel()
+ )
}
public func allSelfUserClientsAreActiveMLSClients() async -> Bool {
- let selfUser = await userRepository.fetchSelfUser()
-
- return await context.perform {
- selfUser.clients.all { userClient in
- let hasMLSIdentity = !userClient.mlsPublicKeys.isEmpty
-
- let isRecentlyActive: Bool = {
- if userClient.isSelfClient() {
- return true
- }
-
- guard let lastActiveDate = userClient.lastActiveDate else {
- return false
- }
-
- guard lastActiveDate <= Date() else {
- return true
- }
-
- return lastActiveDate.timeIntervalSinceNow.magnitude < .fourWeeks
- }()
-
- return hasMLSIdentity && isRecentlyActive
- }
- }
+ await userClientsLocalStore.allSelfUserClientsAreActiveMLSClients()
}
}
diff --git a/WireDomain/Sources/WireDomain/OneOnOneResolver.swift b/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift
similarity index 73%
rename from WireDomain/Sources/WireDomain/OneOnOneResolver.swift
rename to WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift
index a6a326fb96d..91c54b7b37b 100644
--- a/WireDomain/Sources/WireDomain/OneOnOneResolver.swift
+++ b/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift
@@ -19,21 +19,21 @@
import CoreData
import WireAPI
import WireDataModel
+import WireLogging
// sourcery: AutoMockable
/// Resolves 1:1 conversations
public protocol OneOnOneResolverProtocol {
- func invoke() async throws
+
+ func resolveOneOnOneConversation(
+ with userID: WireDataModel.QualifiedID
+ ) async throws
+
+ func resolveAllOneOnOneConversations() async throws
}
struct OneOnOneResolver: OneOnOneResolverProtocol {
- /// The target to resolve 1:1 conversation for.
- enum Target {
- case user(id: WireAPI.QualifiedID)
- case allUsers
- }
-
private enum Error: Swift.Error {
case failedToActivateConversation
case failedToFetchConversation
@@ -43,11 +43,9 @@ struct OneOnOneResolver: OneOnOneResolverProtocol {
// MARK: - Properties
private let context: NSManagedObjectContext
- private let target: Target
private let userRepository: any UserRepositoryProtocol
private let conversationsRepository: any ConversationRepositoryProtocol
- private let mlsService: any MLSServiceInterface
- private let isMLSEnabled: Bool
+ private let mlsProvider: MLSProvider
// MARK: - Object lifecycle
@@ -55,50 +53,50 @@ struct OneOnOneResolver: OneOnOneResolverProtocol {
context: NSManagedObjectContext,
userRepository: any UserRepositoryProtocol,
conversationsRepository: any ConversationRepositoryProtocol,
- mlsService: any MLSServiceInterface,
- isMLSEnabled: Bool,
- target: Target
+ mlsProvider: MLSProvider
) {
self.context = context
self.userRepository = userRepository
self.conversationsRepository = conversationsRepository
- self.mlsService = mlsService
- self.isMLSEnabled = isMLSEnabled
- self.target = target
+ self.mlsProvider = mlsProvider
}
- // MARK: - Public
-
- public func invoke() async throws {
- switch target {
- case let .user(id):
- try await resolveOneOnOneConversation(with: id.toDomainModel())
- case .allUsers:
- // TODO: [WPB-10727] resolve all users 1:1 conversations
- break
+ func resolveAllOneOnOneConversations() async throws {
+ let usersIDs = try await userRepository.fetchAllUserIDsWithOneOnOneConversation()
+
+ await withTaskGroup(of: Void.self) { group in
+ for userID in usersIDs {
+ group.addTask {
+ do {
+ try await resolveOneOnOneConversation(with: userID)
+ } catch {
+ /// skip conversation migration for this user
+ WireLogger.conversation.error(
+ "resolve 1-1 conversation with userID \(userID) failed!"
+ )
+ }
+ }
+ }
}
}
- // MARK: - Private
-
- private func resolveOneOnOneConversation(
+ func resolveOneOnOneConversation(
with userID: WireDataModel.QualifiedID
) async throws {
let user = try await userRepository.fetchUser(
- id: userID.uuid,
- domain: userID.domain
+ id: userID.uuid, domain: userID.domain
)
let selfUser = await userRepository.fetchSelfUser()
let commonProtocol = await getCommonProtocol(between: selfUser, and: user)
- if isMLSEnabled, commonProtocol == .mls {
+ if mlsProvider.isMLSEnabled, commonProtocol == .mls {
try await resolveMLSConversation(
for: user
)
}
- if isMLSEnabled, commonProtocol == nil {
+ if mlsProvider.isMLSEnabled, commonProtocol == nil {
await resolveNoCommonProtocolConversation(
between: selfUser,
and: user
@@ -130,27 +128,27 @@ struct OneOnOneResolver: OneOnOneResolverProtocol {
)
/// Then, fetch the synced MLS conversation.
- let mlsConversation = await conversationsRepository.fetchMLSConversation(
- groupID: mlsGroupID
- )
+ let mlsConversation = await conversationsRepository.fetchMLSConversation(groupID: mlsGroupID)
- let localMLSGroupID = await context.perform {
+ let groupID = await context.perform {
mlsConversation?.mlsGroupID
}
- guard let mlsConversation, let localMLSGroupID else {
+ guard let mlsConversation, let groupID else {
throw Error.failedToFetchConversation
}
+ let mlsService = mlsProvider.service
+
/// If conversation already exists, there is no need to perform a migration.
let needsMLSMigration = try await mlsService.conversationExists(
- groupID: localMLSGroupID
+ groupID: groupID
) == false
if needsMLSMigration {
await migrateToMLS(
mlsConversation: mlsConversation,
- mlsGroupID: localMLSGroupID,
+ mlsGroupID: groupID,
user: user,
userID: userID
)
@@ -197,6 +195,8 @@ struct OneOnOneResolver: OneOnOneResolverProtocol {
groupID: MLSGroupID,
userID: WireDataModel.QualifiedID
) async throws {
+ let mlsService = mlsProvider.service
+
let epoch = await context.perform {
mlsConversation.epoch
}
@@ -253,19 +253,15 @@ struct OneOnOneResolver: OneOnOneResolverProtocol {
private func resolveProteusConversation(
for user: ZMUser
) async {
- WireLogger.conversation.debug("Should resolve to Proteus 1-1 conversation")
-
- let conversation = await context.perform {
- user.oneOnOneConversation
- }
+ await context.perform {
+ WireLogger.conversation.debug("Should resolve to Proteus 1-1 conversation")
- guard let conversation else {
- return WireLogger.conversation.warn(
- "Failed to resolve Proteus conversation: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)"
- )
- }
+ guard let conversation = user.oneOnOneConversation else {
+ return WireLogger.conversation.warn(
+ "Failed to resolve Proteus conversation: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)"
+ )
+ }
- await context.perform {
conversation.isForcedReadOnly = false
}
}
@@ -281,24 +277,16 @@ struct OneOnOneResolver: OneOnOneResolverProtocol {
between selfUser: ZMUser,
and user: ZMUser
) async {
- WireLogger.conversation.debug("No common protocols found")
-
- let conversation = await context.perform {
- user.oneOnOneConversation
- }
-
- guard let conversation else {
- return WireLogger.conversation.warn(
- "Failed to resolve 1:1 conversation with no common protocol: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)"
- )
- }
+ await context.perform {
+ WireLogger.conversation.debug("No common protocols found")
- let isForcedReadOnly = await context.perform {
- conversation.isForcedReadOnly
- }
+ guard let conversation = user.oneOnOneConversation else {
+ return WireLogger.conversation.warn(
+ "Failed to resolve 1:1 conversation with no common protocol: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)"
+ )
+ }
- if !isForcedReadOnly {
- await context.perform {
+ if !conversation.isForcedReadOnly {
if !selfUser.supportedProtocols.contains(.mls) {
conversation.appendMLSMigrationMLSNotSupportedForSelfUser(user: selfUser)
} else if !user.supportedProtocols.contains(.mls) {
@@ -314,22 +302,20 @@ struct OneOnOneResolver: OneOnOneResolverProtocol {
between selfUser: ZMUser,
and otherUser: ZMUser
) async -> ConversationMessageProtocol? {
- let selfUserProtocols = await context.perform {
- selfUser.supportedProtocols
- }
-
- let otherUserProtocols = await context.perform {
- otherUser.supportedProtocols.isEmpty ? [.proteus] : otherUser.supportedProtocols
- }
-
- let commonProtocols = selfUserProtocols.intersection(otherUserProtocols)
-
- if commonProtocols.contains(.mls) {
- return .mls
- } else if commonProtocols.contains(.proteus) {
- return .proteus
- } else {
- return nil
+ await context.perform {
+ let selfUserProtocols = selfUser.supportedProtocols
+ let otherUserProtocols = otherUser.supportedProtocols.isEmpty ? [.proteus] : otherUser
+ .supportedProtocols /// default to Proteus if empty.
+
+ let commonProtocols = selfUserProtocols.intersection(otherUserProtocols)
+
+ if commonProtocols.contains(.mls) {
+ return .mls
+ } else if commonProtocols.contains(.proteus) {
+ return .proteus
+ } else {
+ return nil
+ }
}
}
}
diff --git a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift
index ebee591d656..243fee52944 100644
--- a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift
+++ b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift
@@ -16,12 +16,19 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
+import CoreData
import Foundation
import WireAPI
+import WireDataModel
+import WireLogging
import WireSystem
protocol SyncManagerProtocol {
+ /// Pulls and stores all required objects for the database to be initially up-to-date.
+
+ func performSlowSync() async throws
+
/// Fetch events from the server and process all pending events.
func performQuickSync() async throws
@@ -34,18 +41,88 @@ protocol SyncManagerProtocol {
final class SyncManager: SyncManagerProtocol {
+ enum Failure: Error {
+ case failedToPerformSlowSync(Error)
+ }
+
+ // MARK: - Properties
+
private(set) var syncState: SyncState = .suspended
private var isSuspending = false
+ // MARK: - Repositories
+
private let updateEventsRepository: any UpdateEventsRepositoryProtocol
+ private let teamRepository: any TeamRepositoryProtocol
+ private let connectionsRepository: any ConnectionsRepositoryProtocol
+ private let conversationsRepository: any ConversationRepositoryProtocol
+ private let userRepository: any UserRepositoryProtocol
+ private let conversationLabelsRepository: any ConversationLabelsRepositoryProtocol
+ private let featureConfigsRepository: any FeatureConfigRepositoryProtocol
+ private let pushSupportedProtocolsUseCase: any PushSupportedProtocolsUseCaseProtocol
+ private let mlsProvider: MLSProvider
+ private let context: NSManagedObjectContext
+
+ // MARK: - Update event processor
+
private let updateEventProcessor: any UpdateEventProcessorProtocol
+ // MARK: - Object lifecycle
+
init(
updateEventsRepository: any UpdateEventsRepositoryProtocol,
- updateEventProcessor: any UpdateEventProcessorProtocol
+ teamRepository: any TeamRepositoryProtocol,
+ connectionsRepository: any ConnectionsRepositoryProtocol,
+ conversationsRepository: any ConversationRepositoryProtocol,
+ userRepository: any UserRepositoryProtocol,
+ conversationLabelsRepository: any ConversationLabelsRepositoryProtocol,
+ featureConfigsRepository: any FeatureConfigRepositoryProtocol,
+ updateEventProcessor: any UpdateEventProcessorProtocol,
+ pushSupportedProtocolsUseCase: any PushSupportedProtocolsUseCaseProtocol,
+ mlsProvider: MLSProvider,
+ context: NSManagedObjectContext
) {
self.updateEventsRepository = updateEventsRepository
+ self.teamRepository = teamRepository
+ self.connectionsRepository = connectionsRepository
+ self.conversationsRepository = conversationsRepository
+ self.userRepository = userRepository
+ self.conversationLabelsRepository = conversationLabelsRepository
+ self.featureConfigsRepository = featureConfigsRepository
self.updateEventProcessor = updateEventProcessor
+ self.pushSupportedProtocolsUseCase = pushSupportedProtocolsUseCase
+ self.mlsProvider = mlsProvider
+ self.context = context
+ }
+
+ func performSlowSync() async throws {
+ do {
+ try await updateEventsRepository.pullLastEventID()
+ try await teamRepository.pullSelfTeam()
+ try await teamRepository.pullSelfTeamRoles()
+ try await teamRepository.pullSelfTeamMembers()
+ try await connectionsRepository.pullConnections()
+ try await conversationsRepository.pullConversations()
+ try await userRepository.pullKnownUsers()
+ try await userRepository.pullSelfUser()
+ try await teamRepository.pullSelfLegalholdInfo()
+ try await conversationLabelsRepository.pullConversationLabels()
+ try await featureConfigsRepository.pullFeatureConfigs()
+ try await pushSupportedProtocolsUseCase.invoke()
+ let oneOnOneResolver = makeOneOnOneResolver()
+ try await oneOnOneResolver.resolveAllOneOnOneConversations()
+ } catch {
+ throw Failure.failedToPerformSlowSync(error)
+ }
+ }
+
+ private func makeOneOnOneResolver() -> OneOnOneResolverProtocol {
+ OneOnOneResolver(
+ context: context,
+ userRepository: userRepository,
+ conversationsRepository: conversationsRepository,
+ mlsProvider: mlsProvider
+ )
}
func performQuickSync() async throws {
@@ -111,7 +188,7 @@ final class SyncManager: SyncManagerProtocol {
isSuspending = false
}
- private var ongoingTask: Task? {
+ private var ongoingTask: Task? {
switch syncState {
case let .quickSync(task):
task
diff --git a/WireDomain/Sources/WireDomain/UseCases/IndividualToTeamMigrationUseCaseImplementation.swift b/WireDomain/Sources/WireDomain/UseCases/IndividualToTeamMigrationUseCaseImplementation.swift
new file mode 100644
index 00000000000..0d4cc1aab90
--- /dev/null
+++ b/WireDomain/Sources/WireDomain/UseCases/IndividualToTeamMigrationUseCaseImplementation.swift
@@ -0,0 +1,49 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+@preconcurrency import WireAPI
+import WireDomainAPI
+import WireLogging
+import WireSystem
+
+public struct IndividualToTeamMigrationUseCaseImplementation: IndividualToTeamMigrationUseCase {
+ private let accountsAPI: AccountsAPI
+ private let logger: WireLogger = .individualToTeamMigration
+
+ public init(apiService: APIServiceProtocol, apiVersion: APIVersion) {
+ self.accountsAPI = AccountsAPIBuilder(apiService: apiService).makeAPI(for: apiVersion)
+ }
+
+ public func invoke(teamName: String) async throws -> IndividualToTeamMigrationResult {
+ logger.debug("Migrating individual account to team account")
+ do {
+ let upgradeResult = try await accountsAPI.upgradeToTeam(teamName: teamName)
+ logger.debug("Individual account migrated to team account")
+ return IndividualToTeamMigrationResult(teamID: upgradeResult.teamId, teamName: upgradeResult.teamName)
+ } catch {
+ logger.error("Failed to migrate individual account to team account")
+ switch error {
+ case AccountsAPIError.userAlreadyInATeam:
+ throw IndividualToTeamMigrationError.userAlreadyInTeam
+ default:
+ throw IndividualToTeamMigrationError.generic(error)
+ }
+ }
+ }
+}
diff --git a/WireDomain/Sources/WireDomain/UseCases/PushSupportedProtocolsUseCase.swift b/WireDomain/Sources/WireDomain/UseCases/PushSupportedProtocolsUseCase.swift
index 1ba76b21a5e..43e7e83198e 100644
--- a/WireDomain/Sources/WireDomain/UseCases/PushSupportedProtocolsUseCase.swift
+++ b/WireDomain/Sources/WireDomain/UseCases/PushSupportedProtocolsUseCase.swift
@@ -18,10 +18,16 @@
import WireAPI
import WireDataModel
+import WireLogging
import WireSystem
+// sourcery: AutoMockable
/// Calculates and pushes the supported protocols to the backend
-public struct PushSupportedProtocolsUseCase {
+public protocol PushSupportedProtocolsUseCaseProtocol {
+ func invoke() async throws
+}
+
+public struct PushSupportedProtocolsUseCase: PushSupportedProtocolsUseCaseProtocol {
private enum ProteusToMLSMigrationState: String {
case disabled
@@ -91,7 +97,7 @@ public struct PushSupportedProtocolsUseCase {
private func remotelySupportedProtocols() async -> Set {
let mlsFeature = try? await featureConfigRepository.fetchFeatureConfig(
- with: .mls,
+ name: .mls,
type: Feature.MLS.Config.self
)
@@ -120,7 +126,7 @@ public struct PushSupportedProtocolsUseCase {
private func currentMigrationState() async -> ProteusToMLSMigrationState {
let mlsMigrationFeature = try? await featureConfigRepository.fetchFeatureConfig(
- with: .mlsMigration,
+ name: .mlsMigration,
type: Feature.MLSMigration.Config.self
)
diff --git a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationCodeUpdateEventProcessor.swift b/WireDomain/Sources/WireDomainAPI/UseCases/IndividualToTeamMigrationUseCase.swift
similarity index 54%
rename from WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationCodeUpdateEventProcessor.swift
rename to WireDomain/Sources/WireDomainAPI/UseCases/IndividualToTeamMigrationUseCase.swift
index 03108b5a141..04db1a4525c 100644
--- a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationCodeUpdateEventProcessor.swift
+++ b/WireDomain/Sources/WireDomainAPI/UseCases/IndividualToTeamMigrationUseCase.swift
@@ -16,25 +16,25 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//
-import WireAPI
-
-/// Process conversation code update events.
-
-protocol ConversationCodeUpdateEventProcessorProtocol {
-
- /// Process a conversation code update event.
- ///
- /// - Parameter event: A conversation code update event.
-
- func processEvent(_ event: ConversationCodeUpdateEvent) async throws
+import Foundation
+public enum IndividualToTeamMigrationError: Error, Sendable {
+ case userAlreadyInTeam
+ case generic(any Error)
}
-struct ConversationCodeUpdateEventProcessor: ConversationCodeUpdateEventProcessorProtocol {
+public struct IndividualToTeamMigrationResult: Sendable {
+ public let teamID: UUID
+ public let teamName: String
- func processEvent(_: ConversationCodeUpdateEvent) async throws {
- // TODO: [WPB-10165]
- assertionFailure("not implemented yet")
+ public init(teamID: UUID, teamName: String) {
+ self.teamID = teamID
+ self.teamName = teamName
}
+}
+// sourcery: AutoMockable
+/// Sends a request to the backend to migrate the user to a team.
+public protocol IndividualToTeamMigrationUseCase: Sendable {
+ func invoke(teamName: String) async throws -> IndividualToTeamMigrationResult
}
diff --git a/WireDomain/Sources/WireDomainSupport/Sourcery/config.yml b/WireDomain/Sources/WireDomainSupport/Sourcery/config.yml
index b6f70e203bb..ba382d6f9b3 100644
--- a/WireDomain/Sources/WireDomainSupport/Sourcery/config.yml
+++ b/WireDomain/Sources/WireDomainSupport/Sourcery/config.yml
@@ -5,6 +5,5 @@ templates:
output:
./generated
args:
- autoMockableTestableImports: ["WireDomain"]
autoMockableImports: ["WireAPI", "WireDataModel"]
-
+ autoMockableTestableImports: ["WireDomain"]
diff --git a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift
index d328dbe9036..9452a00a877 100644
--- a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift
+++ b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift
@@ -24,15 +24,9 @@
// swiftlint:disable line_length
// swiftlint:disable variable_name
-public import Foundation
-#if os(iOS) || os(tvOS) || os(watchOS)
-public import UIKit
-#elseif os(OSX)
-public import AppKit
-#endif
-public import WireAPI
-public import WireDataModel
+import WireAPI
+import WireDataModel
@testable import WireDomain
@@ -55,6 +49,35 @@ public import WireDataModel
+
+public class MockConnectionsLocalStoreProtocol: ConnectionsLocalStoreProtocol {
+
+ // MARK: - Life cycle
+
+ public init() {}
+
+
+ // MARK: - storeConnection
+
+ public var storeConnection_Invocations: [ConnectionInfo] = []
+ public var storeConnection_MockError: Error?
+ public var storeConnection_MockMethod: ((ConnectionInfo) async throws -> Void)?
+
+ public func storeConnection(_ connectionInfo: ConnectionInfo) async throws {
+ storeConnection_Invocations.append(connectionInfo)
+
+ if let error = storeConnection_MockError {
+ throw error
+ }
+
+ guard let mock = storeConnection_MockMethod else {
+ fatalError("no mock for `storeConnection`")
+ }
+
+ try await mock(connectionInfo)
+ }
+
+}
public class MockConnectionsRepositoryProtocol: ConnectionsRepositoryProtocol {
@@ -105,6 +128,55 @@ public class MockConnectionsRepositoryProtocol: ConnectionsRepositoryProtocol {
}
+public class MockConversationLabelsLocalStoreProtocol: ConversationLabelsLocalStoreProtocol {
+
+ // MARK: - Life cycle
+
+ public init() {}
+
+
+ // MARK: - storeLabel
+
+ public var storeLabel_Invocations: [ConversationLabelInfo] = []
+ public var storeLabel_MockError: Error?
+ public var storeLabel_MockMethod: ((ConversationLabelInfo) async throws -> Void)?
+
+ public func storeLabel(_ conversationLabel: ConversationLabelInfo) async throws {
+ storeLabel_Invocations.append(conversationLabel)
+
+ if let error = storeLabel_MockError {
+ throw error
+ }
+
+ guard let mock = storeLabel_MockMethod else {
+ fatalError("no mock for `storeLabel`")
+ }
+
+ try await mock(conversationLabel)
+ }
+
+ // MARK: - deleteOldLabelsLocally
+
+ public var deleteOldLabelsLocallyExcludedLabels_Invocations: [[ConversationLabelInfo]] = []
+ public var deleteOldLabelsLocallyExcludedLabels_MockError: Error?
+ public var deleteOldLabelsLocallyExcludedLabels_MockMethod: (([ConversationLabelInfo]) async throws -> Void)?
+
+ public func deleteOldLabelsLocally(excludedLabels: [ConversationLabelInfo]) async throws {
+ deleteOldLabelsLocallyExcludedLabels_Invocations.append(excludedLabels)
+
+ if let error = deleteOldLabelsLocallyExcludedLabels_MockError {
+ throw error
+ }
+
+ guard let mock = deleteOldLabelsLocallyExcludedLabels_MockMethod else {
+ fatalError("no mock for `deleteOldLabelsLocallyExcludedLabels`")
+ }
+
+ try await mock(excludedLabels)
+ }
+
+}
+
public class MockConversationLabelsRepositoryProtocol: ConversationLabelsRepositoryProtocol {
// MARK: - Life cycle
@@ -181,47 +253,47 @@ public class MockConversationLocalStoreProtocol: ConversationLocalStoreProtocol
// MARK: - storeConversation
- public var storeConversationTimestampIsFederationEnabled_Invocations: [(conversation: WireAPI.Conversation, timestamp: Date, isFederationEnabled: Bool)] = []
- public var storeConversationTimestampIsFederationEnabled_MockMethod: ((WireAPI.Conversation, Date, Bool) async -> Void)?
+ public var storeConversationTimestampIsFederationEnabledIsMLSEnabled_Invocations: [(conversation: WireDomain.Conversation, timestamp: Date, isFederationEnabled: Bool, isMLSEnabled: Bool)] = []
+ public var storeConversationTimestampIsFederationEnabledIsMLSEnabled_MockMethod: ((WireDomain.Conversation, Date, Bool, Bool) async -> Void)?
- public func storeConversation(_ conversation: WireAPI.Conversation, timestamp: Date, isFederationEnabled: Bool) async {
- storeConversationTimestampIsFederationEnabled_Invocations.append((conversation: conversation, timestamp: timestamp, isFederationEnabled: isFederationEnabled))
+ public func storeConversation(_ conversation: WireDomain.Conversation, timestamp: Date, isFederationEnabled: Bool, isMLSEnabled: Bool) async {
+ storeConversationTimestampIsFederationEnabledIsMLSEnabled_Invocations.append((conversation: conversation, timestamp: timestamp, isFederationEnabled: isFederationEnabled, isMLSEnabled: isMLSEnabled))
- guard let mock = storeConversationTimestampIsFederationEnabled_MockMethod else {
- fatalError("no mock for `storeConversationTimestampIsFederationEnabled`")
+ guard let mock = storeConversationTimestampIsFederationEnabledIsMLSEnabled_MockMethod else {
+ fatalError("no mock for `storeConversationTimestampIsFederationEnabledIsMLSEnabled`")
}
- await mock(conversation, timestamp, isFederationEnabled)
+ await mock(conversation, timestamp, isFederationEnabled, isMLSEnabled)
}
// MARK: - storeConversation
- public var storeConversationNeedsBackendUpdateQualifiedId_Invocations: [(needsBackendUpdate: Bool, qualifiedId: WireAPI.QualifiedID)] = []
- public var storeConversationNeedsBackendUpdateQualifiedId_MockMethod: ((Bool, WireAPI.QualifiedID) async -> Void)?
+ public var storeConversationNeedsBackendUpdateConversationIDConversationDomain_Invocations: [(needsBackendUpdate: Bool, conversationID: UUID, conversationDomain: String)] = []
+ public var storeConversationNeedsBackendUpdateConversationIDConversationDomain_MockMethod: ((Bool, UUID, String) async -> Void)?
- public func storeConversation(needsBackendUpdate: Bool, qualifiedId: WireAPI.QualifiedID) async {
- storeConversationNeedsBackendUpdateQualifiedId_Invocations.append((needsBackendUpdate: needsBackendUpdate, qualifiedId: qualifiedId))
+ public func storeConversation(needsBackendUpdate: Bool, conversationID: UUID, conversationDomain: String) async {
+ storeConversationNeedsBackendUpdateConversationIDConversationDomain_Invocations.append((needsBackendUpdate: needsBackendUpdate, conversationID: conversationID, conversationDomain: conversationDomain))
- guard let mock = storeConversationNeedsBackendUpdateQualifiedId_MockMethod else {
- fatalError("no mock for `storeConversationNeedsBackendUpdateQualifiedId`")
+ guard let mock = storeConversationNeedsBackendUpdateConversationIDConversationDomain_MockMethod else {
+ fatalError("no mock for `storeConversationNeedsBackendUpdateConversationIDConversationDomain`")
}
- await mock(needsBackendUpdate, qualifiedId)
+ await mock(needsBackendUpdate, conversationID, conversationDomain)
}
// MARK: - storeFailedConversation
- public var storeFailedConversationWithQualifiedId_Invocations: [WireAPI.QualifiedID] = []
- public var storeFailedConversationWithQualifiedId_MockMethod: ((WireAPI.QualifiedID) async -> Void)?
+ public var storeFailedConversationConversationIDConversationDomain_Invocations: [(conversationID: UUID, conversationDomain: String)] = []
+ public var storeFailedConversationConversationIDConversationDomain_MockMethod: ((UUID, String) async -> Void)?
- public func storeFailedConversation(withQualifiedId qualifiedId: WireAPI.QualifiedID) async {
- storeFailedConversationWithQualifiedId_Invocations.append(qualifiedId)
+ public func storeFailedConversation(conversationID: UUID, conversationDomain: String) async {
+ storeFailedConversationConversationIDConversationDomain_Invocations.append((conversationID: conversationID, conversationDomain: conversationDomain))
- guard let mock = storeFailedConversationWithQualifiedId_MockMethod else {
- fatalError("no mock for `storeFailedConversationWithQualifiedId`")
+ guard let mock = storeFailedConversationConversationIDConversationDomain_MockMethod else {
+ fatalError("no mock for `storeFailedConversationConversationIDConversationDomain`")
}
- await mock(qualifiedId)
+ await mock(conversationID, conversationDomain)
}
// MARK: - fetchMLSConversation
@@ -282,17 +354,22 @@ public class MockConversationLocalStoreProtocol: ConversationLocalStoreProtocol
// MARK: - removeParticipantFromAllGroupConversations
- public var removeParticipantFromAllGroupConversationsUserDate_Invocations: [(user: ZMUser, date: Date)] = []
- public var removeParticipantFromAllGroupConversationsUserDate_MockMethod: ((ZMUser, Date) async -> Void)?
+ public var removeParticipantFromAllGroupConversationsParticipantIDParticipantDomainDate_Invocations: [(participantID: UUID, participantDomain: String?, date: Date)] = []
+ public var removeParticipantFromAllGroupConversationsParticipantIDParticipantDomainDate_MockError: Error?
+ public var removeParticipantFromAllGroupConversationsParticipantIDParticipantDomainDate_MockMethod: ((UUID, String?, Date) async throws -> Void)?
- public func removeParticipantFromAllGroupConversations(user: ZMUser, date: Date) async {
- removeParticipantFromAllGroupConversationsUserDate_Invocations.append((user: user, date: date))
+ public func removeParticipantFromAllGroupConversations(participantID: UUID, participantDomain: String?, date: Date) async throws {
+ removeParticipantFromAllGroupConversationsParticipantIDParticipantDomainDate_Invocations.append((participantID: participantID, participantDomain: participantDomain, date: date))
- guard let mock = removeParticipantFromAllGroupConversationsUserDate_MockMethod else {
- fatalError("no mock for `removeParticipantFromAllGroupConversationsUserDate`")
+ if let error = removeParticipantFromAllGroupConversationsParticipantIDParticipantDomainDate_MockError {
+ throw error
}
- await mock(user, date)
+ guard let mock = removeParticipantFromAllGroupConversationsParticipantIDParticipantDomainDate_MockMethod else {
+ fatalError("no mock for `removeParticipantFromAllGroupConversationsParticipantIDParticipantDomainDate`")
+ }
+
+ try await mock(participantID, participantDomain, date)
}
// MARK: - addOrUpdateParticipant
@@ -312,19 +389,19 @@ public class MockConversationLocalStoreProtocol: ConversationLocalStoreProtocol
// MARK: - addParticipants
- public var addParticipantsAddedByAtDateTo_Invocations: [(participants: [(id: UUID, domain: String?, role: String?)], sender: (id: UUID, domain: String?), date: Date, conversation: ZMConversation)] = []
- public var addParticipantsAddedByAtDateTo_MockError: Error?
- public var addParticipantsAddedByAtDateTo_MockMethod: (([(id: UUID, domain: String?, role: String?)], (id: UUID, domain: String?), Date, ZMConversation) async throws -> Void)?
+ public var addParticipantsAddedByAtDateConversation_Invocations: [(participants: [(id: UUID, domain: String?, role: String?)], sender: (id: UUID, domain: String?), date: Date, conversation: (id: UUID, domain: String))] = []
+ public var addParticipantsAddedByAtDateConversation_MockError: Error?
+ public var addParticipantsAddedByAtDateConversation_MockMethod: (([(id: UUID, domain: String?, role: String?)], (id: UUID, domain: String?), Date, (id: UUID, domain: String)) async throws -> Void)?
- public func addParticipants(_ participants: [(id: UUID, domain: String?, role: String?)], addedBy sender: (id: UUID, domain: String?), atDate date: Date, to conversation: ZMConversation) async throws {
- addParticipantsAddedByAtDateTo_Invocations.append((participants: participants, sender: sender, date: date, conversation: conversation))
+ public func addParticipants(_ participants: [(id: UUID, domain: String?, role: String?)], addedBy sender: (id: UUID, domain: String?), atDate date: Date, conversation: (id: UUID, domain: String)) async throws {
+ addParticipantsAddedByAtDateConversation_Invocations.append((participants: participants, sender: sender, date: date, conversation: conversation))
- if let error = addParticipantsAddedByAtDateTo_MockError {
+ if let error = addParticipantsAddedByAtDateConversation_MockError {
throw error
}
- guard let mock = addParticipantsAddedByAtDateTo_MockMethod else {
- fatalError("no mock for `addParticipantsAddedByAtDateTo`")
+ guard let mock = addParticipantsAddedByAtDateConversation_MockMethod else {
+ fatalError("no mock for `addParticipantsAddedByAtDateConversation`")
}
try await mock(participants, sender, date, conversation)
@@ -378,100 +455,67 @@ public class MockConversationLocalStoreProtocol: ConversationLocalStoreProtocol
}
}
- // MARK: - addSystemMessage
+ // MARK: - storeConversation
- public var addSystemMessageTo_Invocations: [(message: SystemMessage, conversation: ZMConversation)] = []
- public var addSystemMessageTo_MockMethod: ((SystemMessage, ZMConversation) async -> Void)?
+ public var storeConversationHasReadReceiptsEnabledFor_Invocations: [(hasReadReceiptsEnabled: Bool, conversation: ZMConversation)] = []
+ public var storeConversationHasReadReceiptsEnabledFor_MockMethod: ((Bool, ZMConversation) async -> Void)?
- public func addSystemMessage(_ message: SystemMessage, to conversation: ZMConversation) async {
- addSystemMessageTo_Invocations.append((message: message, conversation: conversation))
+ public func storeConversation(hasReadReceiptsEnabled: Bool, for conversation: ZMConversation) async {
+ storeConversationHasReadReceiptsEnabledFor_Invocations.append((hasReadReceiptsEnabled: hasReadReceiptsEnabled, conversation: conversation))
- guard let mock = addSystemMessageTo_MockMethod else {
- fatalError("no mock for `addSystemMessageTo`")
+ guard let mock = storeConversationHasReadReceiptsEnabledFor_MockMethod else {
+ fatalError("no mock for `storeConversationHasReadReceiptsEnabledFor`")
}
- await mock(message, conversation)
- }
-
- // MARK: - conversationMutedMessageTypes
-
- public var conversationMutedMessageTypes_Invocations: [ZMConversation] = []
- public var conversationMutedMessageTypes_MockMethod: ((ZMConversation) async -> MutedMessageTypes)?
- public var conversationMutedMessageTypes_MockValue: MutedMessageTypes?
-
- public func conversationMutedMessageTypes(_ conversation: ZMConversation) async -> MutedMessageTypes {
- conversationMutedMessageTypes_Invocations.append(conversation)
-
- if let mock = conversationMutedMessageTypes_MockMethod {
- return await mock(conversation)
- } else if let mock = conversationMutedMessageTypes_MockValue {
- return mock
- } else {
- fatalError("no mock for `conversationMutedMessageTypes`")
- }
+ await mock(hasReadReceiptsEnabled, conversation)
}
- // MARK: - storeConversation
+ // MARK: - removeParticipantsAndUpdateConversationState
- public var storeConversationIsArchivedFor_Invocations: [(isArchived: Bool, conversation: ZMConversation)] = []
- public var storeConversationIsArchivedFor_MockMethod: ((Bool, ZMConversation) async -> Void)?
+ public var removeParticipantsAndUpdateConversationStateConversationUsersInitiatingUser_Invocations: [(conversation: ZMConversation, users: Set, initiatingUser: ZMUser)] = []
+ public var removeParticipantsAndUpdateConversationStateConversationUsersInitiatingUser_MockMethod: ((ZMConversation, Set, ZMUser) async -> Void)?
- public func storeConversation(isArchived: Bool, for conversation: ZMConversation) async {
- storeConversationIsArchivedFor_Invocations.append((isArchived: isArchived, conversation: conversation))
+ public func removeParticipantsAndUpdateConversationState(conversation: ZMConversation, users: Set, initiatingUser: ZMUser) async {
+ removeParticipantsAndUpdateConversationStateConversationUsersInitiatingUser_Invocations.append((conversation: conversation, users: users, initiatingUser: initiatingUser))
- guard let mock = storeConversationIsArchivedFor_MockMethod else {
- fatalError("no mock for `storeConversationIsArchivedFor`")
+ guard let mock = removeParticipantsAndUpdateConversationStateConversationUsersInitiatingUser_MockMethod else {
+ fatalError("no mock for `removeParticipantsAndUpdateConversationStateConversationUsersInitiatingUser`")
}
- await mock(isArchived, conversation)
+ await mock(conversation, users, initiatingUser)
}
- // MARK: - isConversationArchived
+ // MARK: - conversationMessageDestructionTimeout
- public var isConversationArchived_Invocations: [ZMConversation] = []
- public var isConversationArchived_MockMethod: ((ZMConversation) async -> Bool)?
- public var isConversationArchived_MockValue: Bool?
+ public var conversationMessageDestructionTimeout_Invocations: [ZMConversation] = []
+ public var conversationMessageDestructionTimeout_MockMethod: ((ZMConversation) async -> MessageDestructionTimeoutValue)?
+ public var conversationMessageDestructionTimeout_MockValue: MessageDestructionTimeoutValue?
- public func isConversationArchived(_ conversation: ZMConversation) async -> Bool {
- isConversationArchived_Invocations.append(conversation)
+ public func conversationMessageDestructionTimeout(_ conversation: ZMConversation) async -> MessageDestructionTimeoutValue {
+ conversationMessageDestructionTimeout_Invocations.append(conversation)
- if let mock = isConversationArchived_MockMethod {
+ if let mock = conversationMessageDestructionTimeout_MockMethod {
return await mock(conversation)
- } else if let mock = isConversationArchived_MockValue {
+ } else if let mock = conversationMessageDestructionTimeout_MockValue {
return mock
} else {
- fatalError("no mock for `isConversationArchived`")
+ fatalError("no mock for `conversationMessageDestructionTimeout`")
}
}
// MARK: - storeConversation
- public var storeConversationHasReadReceiptsEnabledFor_Invocations: [(hasReadReceiptsEnabled: Bool, conversation: ZMConversation)] = []
- public var storeConversationHasReadReceiptsEnabledFor_MockMethod: ((Bool, ZMConversation) async -> Void)?
+ public var storeConversationTimeoutValueFor_Invocations: [(timeoutValue: Double, conversation: ZMConversation)] = []
+ public var storeConversationTimeoutValueFor_MockMethod: ((Double, ZMConversation) async -> Void)?
- public func storeConversation(hasReadReceiptsEnabled: Bool, for conversation: ZMConversation) async {
- storeConversationHasReadReceiptsEnabledFor_Invocations.append((hasReadReceiptsEnabled: hasReadReceiptsEnabled, conversation: conversation))
-
- guard let mock = storeConversationHasReadReceiptsEnabledFor_MockMethod else {
- fatalError("no mock for `storeConversationHasReadReceiptsEnabledFor`")
- }
-
- await mock(hasReadReceiptsEnabled, conversation)
- }
-
- // MARK: - removeParticipantsAndUpdateConversationState
+ public func storeConversation(timeoutValue: Double, for conversation: ZMConversation) async {
+ storeConversationTimeoutValueFor_Invocations.append((timeoutValue: timeoutValue, conversation: conversation))
- public var removeParticipantsAndUpdateConversationStateConversationUsersInitiatingUser_Invocations: [(conversation: ZMConversation, users: Set, initiatingUser: ZMUser)] = []
- public var removeParticipantsAndUpdateConversationStateConversationUsersInitiatingUser_MockMethod: ((ZMConversation, Set, ZMUser) async -> Void)?
-
- public func removeParticipantsAndUpdateConversationState(conversation: ZMConversation, users: Set, initiatingUser: ZMUser) async {
- removeParticipantsAndUpdateConversationStateConversationUsersInitiatingUser_Invocations.append((conversation: conversation, users: users, initiatingUser: initiatingUser))
-
- guard let mock = removeParticipantsAndUpdateConversationStateConversationUsersInitiatingUser_MockMethod else {
- fatalError("no mock for `removeParticipantsAndUpdateConversationStateConversationUsersInitiatingUser`")
+ guard let mock = storeConversationTimeoutValueFor_MockMethod else {
+ fatalError("no mock for `storeConversationTimeoutValueFor`")
}
- await mock(conversation, users, initiatingUser)
+ await mock(timeoutValue, conversation)
}
// MARK: - fetchOrCreateRole
@@ -594,6 +638,117 @@ public class MockConversationLocalStoreProtocol: ConversationLocalStoreProtocol
}
}
+ // MARK: - updateTypingUsers
+
+ public var updateTypingUsersConversationIDUsersID_Invocations: [(conversationID: NSManagedObjectID, usersID: Set)] = []
+ public var updateTypingUsersConversationIDUsersID_MockMethod: ((NSManagedObjectID, Set) async -> Void)?
+
+ public func updateTypingUsers(conversationID: NSManagedObjectID, usersID: Set) async {
+ updateTypingUsersConversationIDUsersID_Invocations.append((conversationID: conversationID, usersID: usersID))
+
+ guard let mock = updateTypingUsersConversationIDUsersID_MockMethod else {
+ fatalError("no mock for `updateTypingUsersConversationIDUsersID`")
+ }
+
+ await mock(conversationID, usersID)
+ }
+
+ // MARK: - obtainPermanentIDs
+
+ public var obtainPermanentIDsUserConversation_Invocations: [(user: ZMUser, conversation: ZMConversation)] = []
+ public var obtainPermanentIDsUserConversation_MockMethod: ((ZMUser, ZMConversation) -> Void)?
+
+ public func obtainPermanentIDs(user: ZMUser, conversation: ZMConversation) {
+ obtainPermanentIDsUserConversation_Invocations.append((user: user, conversation: conversation))
+
+ guard let mock = obtainPermanentIDsUserConversation_MockMethod else {
+ fatalError("no mock for `obtainPermanentIDsUserConversation`")
+ }
+
+ mock(user, conversation)
+ }
+
+ // MARK: - conversationName
+
+ public var conversationNameConversation_Invocations: [ZMConversation] = []
+ public var conversationNameConversation_MockMethod: ((ZMConversation) async -> String?)?
+ public var conversationNameConversation_MockValue: String??
+
+ public func conversationName(conversation: ZMConversation) async -> String? {
+ conversationNameConversation_Invocations.append(conversation)
+
+ if let mock = conversationNameConversation_MockMethod {
+ return await mock(conversation)
+ } else if let mock = conversationNameConversation_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `conversationNameConversation`")
+ }
+ }
+
+ // MARK: - storeConversation
+
+ public var storeConversationNewNameConversation_Invocations: [(newName: String, conversation: ZMConversation)] = []
+ public var storeConversationNewNameConversation_MockMethod: ((String, ZMConversation) async -> Void)?
+
+ public func storeConversation(newName: String, conversation: ZMConversation) async {
+ storeConversationNewNameConversation_Invocations.append((newName: newName, conversation: conversation))
+
+ guard let mock = storeConversationNewNameConversation_MockMethod else {
+ fatalError("no mock for `storeConversationNewNameConversation`")
+ }
+
+ await mock(newName, conversation)
+ }
+
+ // MARK: - updateOrCreateMLSGroup
+
+ public var updateOrCreateMLSGroupGroupID_Invocations: [MLSGroupID] = []
+ public var updateOrCreateMLSGroupGroupID_MockMethod: ((MLSGroupID) async -> Void)?
+
+ public func updateOrCreateMLSGroup(groupID: MLSGroupID) async {
+ updateOrCreateMLSGroupGroupID_Invocations.append(groupID)
+
+ guard let mock = updateOrCreateMLSGroupGroupID_MockMethod else {
+ fatalError("no mock for `updateOrCreateMLSGroupGroupID`")
+ }
+
+ await mock(groupID)
+ }
+
+ // MARK: - storeMLSConversationEstablished
+
+ public var storeMLSConversationEstablishedMlsGroupIDConversation_Invocations: [(mlsGroupID: MLSGroupID, conversation: ZMConversation)] = []
+ public var storeMLSConversationEstablishedMlsGroupIDConversation_MockMethod: ((MLSGroupID, ZMConversation) async -> Void)?
+
+ public func storeMLSConversationEstablished(mlsGroupID: MLSGroupID, conversation: ZMConversation) async {
+ storeMLSConversationEstablishedMlsGroupIDConversation_Invocations.append((mlsGroupID: mlsGroupID, conversation: conversation))
+
+ guard let mock = storeMLSConversationEstablishedMlsGroupIDConversation_MockMethod else {
+ fatalError("no mock for `storeMLSConversationEstablishedMlsGroupIDConversation`")
+ }
+
+ await mock(mlsGroupID, conversation)
+ }
+
+ // MARK: - fetchOtherUserIDInOneOnOneConversation
+
+ public var fetchOtherUserIDInOneOnOneConversationConversation_Invocations: [ZMConversation] = []
+ public var fetchOtherUserIDInOneOnOneConversationConversation_MockMethod: ((ZMConversation) async -> WireDataModel.QualifiedID?)?
+ public var fetchOtherUserIDInOneOnOneConversationConversation_MockValue: WireDataModel.QualifiedID??
+
+ public func fetchOtherUserIDInOneOnOneConversation(conversation: ZMConversation) async -> WireDataModel.QualifiedID? {
+ fetchOtherUserIDInOneOnOneConversationConversation_Invocations.append(conversation)
+
+ if let mock = fetchOtherUserIDInOneOnOneConversationConversation_MockMethod {
+ return await mock(conversation)
+ } else if let mock = fetchOtherUserIDInOneOnOneConversationConversation_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `fetchOtherUserIDInOneOnOneConversationConversation`")
+ }
+ }
+
}
public class MockConversationRepositoryProtocol: ConversationRepositoryProtocol {
@@ -643,10 +798,10 @@ public class MockConversationRepositoryProtocol: ConversationRepositoryProtocol
// MARK: - storeConversation
- public var storeConversationTimestamp_Invocations: [(conversation: WireAPI.Conversation, timestamp: Date)] = []
- public var storeConversationTimestamp_MockMethod: ((WireAPI.Conversation, Date) async -> Void)?
+ public var storeConversationTimestamp_Invocations: [(conversation: WireDomain.Conversation, timestamp: Date)] = []
+ public var storeConversationTimestamp_MockMethod: ((WireDomain.Conversation, Date) async -> Void)?
- public func storeConversation(_ conversation: WireAPI.Conversation, timestamp: Date) async {
+ public func storeConversation(_ conversation: WireDomain.Conversation, timestamp: Date) async {
storeConversationTimestamp_Invocations.append((conversation: conversation, timestamp: timestamp))
guard let mock = storeConversationTimestamp_MockMethod else {
@@ -830,19 +985,57 @@ public class MockConversationRepositoryProtocol: ConversationRepositoryProtocol
try await mock(userIDs, conversation, sender, date, reason)
}
- // MARK: - addSystemMessage
+ // MARK: - updateConversationName
- public var addSystemMessageTo_Invocations: [(message: SystemMessage, conversation: ZMConversation)] = []
- public var addSystemMessageTo_MockMethod: ((SystemMessage, ZMConversation) async -> Void)?
+ public var updateConversationNameNewNameConversationIDConversationDomainSenderIDSenderDomainDate_Invocations: [(newName: String, conversationID: UUID, conversationDomain: String?, senderID: UUID, senderDomain: String?, date: Date)] = []
+ public var updateConversationNameNewNameConversationIDConversationDomainSenderIDSenderDomainDate_MockMethod: ((String, UUID, String?, UUID, String?, Date) async -> Void)?
- public func addSystemMessage(_ message: SystemMessage, to conversation: ZMConversation) async {
- addSystemMessageTo_Invocations.append((message: message, conversation: conversation))
+ public func updateConversationName(newName: String, conversationID: UUID, conversationDomain: String?, senderID: UUID, senderDomain: String?, date: Date) async {
+ updateConversationNameNewNameConversationIDConversationDomainSenderIDSenderDomainDate_Invocations.append((newName: newName, conversationID: conversationID, conversationDomain: conversationDomain, senderID: senderID, senderDomain: senderDomain, date: date))
- guard let mock = addSystemMessageTo_MockMethod else {
- fatalError("no mock for `addSystemMessageTo`")
+ guard let mock = updateConversationNameNewNameConversationIDConversationDomainSenderIDSenderDomainDate_MockMethod else {
+ fatalError("no mock for `updateConversationNameNewNameConversationIDConversationDomainSenderIDSenderDomainDate`")
}
- await mock(message, conversation)
+ await mock(newName, conversationID, conversationDomain, senderID, senderDomain, date)
+ }
+
+ // MARK: - updateTypingUsers
+
+ public var updateTypingUsers_Invocations: [[ConversationTypingUsersInfo]] = []
+ public var updateTypingUsers_MockMethod: (([ConversationTypingUsersInfo]) async -> Void)?
+
+ public func updateTypingUsers(_ typingUsersInfo: [ConversationTypingUsersInfo]) async {
+ updateTypingUsers_Invocations.append(typingUsersInfo)
+
+ guard let mock = updateTypingUsers_MockMethod else {
+ fatalError("no mock for `updateTypingUsers`")
+ }
+
+ await mock(typingUsersInfo)
+ }
+
+ // MARK: - fetchConversationGuestLink
+
+ public var fetchConversationGuestLinkConversationID_Invocations: [String] = []
+ public var fetchConversationGuestLinkConversationID_MockError: Error?
+ public var fetchConversationGuestLinkConversationID_MockMethod: ((String) async throws -> String?)?
+ public var fetchConversationGuestLinkConversationID_MockValue: String??
+
+ public func fetchConversationGuestLink(conversationID: String) async throws -> String? {
+ fetchConversationGuestLinkConversationID_Invocations.append(conversationID)
+
+ if let error = fetchConversationGuestLinkConversationID_MockError {
+ throw error
+ }
+
+ if let mock = fetchConversationGuestLinkConversationID_MockMethod {
+ return try await mock(conversationID)
+ } else if let mock = fetchConversationGuestLinkConversationID_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `fetchConversationGuestLinkConversationID`")
+ }
}
}
@@ -902,6 +1095,86 @@ public class MockOneOnOneResolverProtocol: OneOnOneResolverProtocol {
public init() {}
+ // MARK: - resolveOneOnOneConversation
+
+ public var resolveOneOnOneConversationWith_Invocations: [WireDataModel.QualifiedID] = []
+ public var resolveOneOnOneConversationWith_MockError: Error?
+ public var resolveOneOnOneConversationWith_MockMethod: ((WireDataModel.QualifiedID) async throws -> Void)?
+
+ public func resolveOneOnOneConversation(with userID: WireDataModel.QualifiedID) async throws {
+ resolveOneOnOneConversationWith_Invocations.append(userID)
+
+ if let error = resolveOneOnOneConversationWith_MockError {
+ throw error
+ }
+
+ guard let mock = resolveOneOnOneConversationWith_MockMethod else {
+ fatalError("no mock for `resolveOneOnOneConversationWith`")
+ }
+
+ try await mock(userID)
+ }
+
+ // MARK: - resolveAllOneOnOneConversations
+
+ public var resolveAllOneOnOneConversations_Invocations: [Void] = []
+ public var resolveAllOneOnOneConversations_MockError: Error?
+ public var resolveAllOneOnOneConversations_MockMethod: (() async throws -> Void)?
+
+ public func resolveAllOneOnOneConversations() async throws {
+ resolveAllOneOnOneConversations_Invocations.append(())
+
+ if let error = resolveAllOneOnOneConversations_MockError {
+ throw error
+ }
+
+ guard let mock = resolveAllOneOnOneConversations_MockMethod else {
+ fatalError("no mock for `resolveAllOneOnOneConversations`")
+ }
+
+ try await mock()
+ }
+
+}
+
+class MockProteusMessageDecryptorProtocol: ProteusMessageDecryptorProtocol {
+
+ // MARK: - Life cycle
+
+
+
+ // MARK: - decryptedEventData
+
+ var decryptedEventDataFrom_Invocations: [ConversationProteusMessageAddEvent] = []
+ var decryptedEventDataFrom_MockError: Error?
+ var decryptedEventDataFrom_MockMethod: ((ConversationProteusMessageAddEvent) async throws -> ConversationProteusMessageAddEvent)?
+ var decryptedEventDataFrom_MockValue: ConversationProteusMessageAddEvent?
+
+ func decryptedEventData(from eventData: ConversationProteusMessageAddEvent) async throws -> ConversationProteusMessageAddEvent {
+ decryptedEventDataFrom_Invocations.append(eventData)
+
+ if let error = decryptedEventDataFrom_MockError {
+ throw error
+ }
+
+ if let mock = decryptedEventDataFrom_MockMethod {
+ return try await mock(eventData)
+ } else if let mock = decryptedEventDataFrom_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `decryptedEventDataFrom`")
+ }
+ }
+
+}
+
+public class MockPushSupportedProtocolsUseCaseProtocol: PushSupportedProtocolsUseCaseProtocol {
+
+ // MARK: - Life cycle
+
+ public init() {}
+
+
// MARK: - invoke
public var invoke_Invocations: [Void] = []
@@ -924,59 +1197,212 @@ public class MockOneOnOneResolverProtocol: OneOnOneResolverProtocol {
}
-class MockProteusMessageDecryptorProtocol: ProteusMessageDecryptorProtocol {
+public class MockSelfUserProviderProtocol: SelfUserProviderProtocol {
+
+ // MARK: - Life cycle
+
+ public init() {}
+
+
+ // MARK: - fetchSelfUser
+
+ public var fetchSelfUser_Invocations: [Void] = []
+ public var fetchSelfUser_MockMethod: (() -> ZMUser)?
+ public var fetchSelfUser_MockValue: ZMUser?
+
+ public func fetchSelfUser() -> ZMUser {
+ fetchSelfUser_Invocations.append(())
+
+ if let mock = fetchSelfUser_MockMethod {
+ return mock()
+ } else if let mock = fetchSelfUser_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `fetchSelfUser`")
+ }
+ }
+
+}
+
+public class MockTeamLocalStoreProtocol: TeamLocalStoreProtocol {
// MARK: - Life cycle
+ public init() {}
+
+
+ // MARK: - fetchMember
+
+ public var fetchMemberId_Invocations: [UUID] = []
+ public var fetchMemberId_MockMethod: ((UUID) async -> Member?)?
+ public var fetchMemberId_MockValue: Member??
+
+ public func fetchMember(id: UUID) async -> Member? {
+ fetchMemberId_Invocations.append(id)
+
+ if let mock = fetchMemberId_MockMethod {
+ return await mock(id)
+ } else if let mock = fetchMemberId_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `fetchMemberId`")
+ }
+ }
+
+ // MARK: - selfUserID
+
+ public var selfUserID_Invocations: [Void] = []
+ public var selfUserID_MockMethod: (() async -> UUID)?
+ public var selfUserID_MockValue: UUID?
+
+ public func selfUserID() async -> UUID {
+ selfUserID_Invocations.append(())
+
+ if let mock = selfUserID_MockMethod {
+ return await mock()
+ } else if let mock = selfUserID_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `selfUserID`")
+ }
+ }
+
+ // MARK: - userMembership
+
+ public var userMembershipUser_Invocations: [ZMUser] = []
+ public var userMembershipUser_MockMethod: ((ZMUser) async -> Member?)?
+ public var userMembershipUser_MockValue: Member??
+
+ public func userMembership(user: ZMUser) async -> Member? {
+ userMembershipUser_Invocations.append(user)
+
+ if let mock = userMembershipUser_MockMethod {
+ return await mock(user)
+ } else if let mock = userMembershipUser_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `userMembershipUser`")
+ }
+ }
+
+ // MARK: - userDomain
+
+ public var userDomainUser_Invocations: [ZMUser] = []
+ public var userDomainUser_MockMethod: ((ZMUser) async -> String?)?
+ public var userDomainUser_MockValue: String??
+
+ public func userDomain(user: ZMUser) async -> String? {
+ userDomainUser_Invocations.append(user)
+
+ if let mock = userDomainUser_MockMethod {
+ return await mock(user)
+ } else if let mock = userDomainUser_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `userDomainUser`")
+ }
+ }
+
+ // MARK: - deleteMember
+
+ public var deleteMember_Invocations: [Member] = []
+ public var deleteMember_MockMethod: ((Member) async -> Void)?
+
+ public func deleteMember(_ member: Member) async {
+ deleteMember_Invocations.append(member)
+
+ guard let mock = deleteMember_MockMethod else {
+ fatalError("no mock for `deleteMember`")
+ }
+
+ await mock(member)
+ }
+
+ // MARK: - storeMember
+
+ public var storeMemberNeedsBackendUpdateMember_Invocations: [(needsBackendUpdate: Bool, member: Member)] = []
+ public var storeMemberNeedsBackendUpdateMember_MockMethod: ((Bool, Member) async -> Void)?
+
+ public func storeMember(needsBackendUpdate: Bool, member: Member) async {
+ storeMemberNeedsBackendUpdateMember_Invocations.append((needsBackendUpdate: needsBackendUpdate, member: member))
+
+ guard let mock = storeMemberNeedsBackendUpdateMember_MockMethod else {
+ fatalError("no mock for `storeMemberNeedsBackendUpdateMember`")
+ }
+
+ await mock(needsBackendUpdate, member)
+ }
+
+ // MARK: - storeTeam
+
+ public var storeTeamIdNameCreatorIDLogoIDLogoKey_Invocations: [(id: UUID, name: String, creatorID: UUID, logoID: String?, logoKey: String?)] = []
+ public var storeTeamIdNameCreatorIDLogoIDLogoKey_MockMethod: ((UUID, String, UUID, String?, String?) async -> Void)?
+
+ public func storeTeam(id: UUID, name: String, creatorID: UUID, logoID: String?, logoKey: String?) async {
+ storeTeamIdNameCreatorIDLogoIDLogoKey_Invocations.append((id: id, name: name, creatorID: creatorID, logoID: logoID, logoKey: logoKey))
+
+ guard let mock = storeTeamIdNameCreatorIDLogoIDLogoKey_MockMethod else {
+ fatalError("no mock for `storeTeamIdNameCreatorIDLogoIDLogoKey`")
+ }
+ await mock(id, name, creatorID, logoID, logoKey)
+ }
- // MARK: - decryptedEventData
+ // MARK: - storeTeamRoles
- var decryptedEventDataFrom_Invocations: [ConversationProteusMessageAddEvent] = []
- var decryptedEventDataFrom_MockError: Error?
- var decryptedEventDataFrom_MockMethod: ((ConversationProteusMessageAddEvent) async throws -> ConversationProteusMessageAddEvent)?
- var decryptedEventDataFrom_MockValue: ConversationProteusMessageAddEvent?
+ public var storeTeamRolesSelfTeamIDTeamRolesInfo_Invocations: [(selfTeamID: UUID, teamRolesInfo: [TeamRoleInfo])] = []
+ public var storeTeamRolesSelfTeamIDTeamRolesInfo_MockError: Error?
+ public var storeTeamRolesSelfTeamIDTeamRolesInfo_MockMethod: ((UUID, [TeamRoleInfo]) async throws -> Void)?
- func decryptedEventData(from eventData: ConversationProteusMessageAddEvent) async throws -> ConversationProteusMessageAddEvent {
- decryptedEventDataFrom_Invocations.append(eventData)
+ public func storeTeamRoles(selfTeamID: UUID, teamRolesInfo: [TeamRoleInfo]) async throws {
+ storeTeamRolesSelfTeamIDTeamRolesInfo_Invocations.append((selfTeamID: selfTeamID, teamRolesInfo: teamRolesInfo))
- if let error = decryptedEventDataFrom_MockError {
+ if let error = storeTeamRolesSelfTeamIDTeamRolesInfo_MockError {
throw error
}
- if let mock = decryptedEventDataFrom_MockMethod {
- return try await mock(eventData)
- } else if let mock = decryptedEventDataFrom_MockValue {
- return mock
- } else {
- fatalError("no mock for `decryptedEventDataFrom`")
+ guard let mock = storeTeamRolesSelfTeamIDTeamRolesInfo_MockMethod else {
+ fatalError("no mock for `storeTeamRolesSelfTeamIDTeamRolesInfo`")
}
+
+ try await mock(selfTeamID, teamRolesInfo)
}
-}
+ // MARK: - storeTeamMembers
-public class MockSelfUserProviderProtocol: SelfUserProviderProtocol {
+ public var storeTeamMembersSelfTeamIDTeamMembersInfo_Invocations: [(selfTeamID: UUID, teamMembersInfo: [TeamMemberInfo])] = []
+ public var storeTeamMembersSelfTeamIDTeamMembersInfo_MockError: Error?
+ public var storeTeamMembersSelfTeamIDTeamMembersInfo_MockMethod: ((UUID, [TeamMemberInfo]) async throws -> Void)?
- // MARK: - Life cycle
+ public func storeTeamMembers(selfTeamID: UUID, teamMembersInfo: [TeamMemberInfo]) async throws {
+ storeTeamMembersSelfTeamIDTeamMembersInfo_Invocations.append((selfTeamID: selfTeamID, teamMembersInfo: teamMembersInfo))
- public init() {}
+ if let error = storeTeamMembersSelfTeamIDTeamMembersInfo_MockError {
+ throw error
+ }
+ guard let mock = storeTeamMembersSelfTeamIDTeamMembersInfo_MockMethod else {
+ fatalError("no mock for `storeTeamMembersSelfTeamIDTeamMembersInfo`")
+ }
- // MARK: - fetchSelfUser
+ try await mock(selfTeamID, teamMembersInfo)
+ }
- public var fetchSelfUser_Invocations: [Void] = []
- public var fetchSelfUser_MockMethod: (() -> ZMUser)?
- public var fetchSelfUser_MockValue: ZMUser?
+ // MARK: - selfUserInfo
- public func fetchSelfUser() -> ZMUser {
- fetchSelfUser_Invocations.append(())
+ public var selfUserInfo_Invocations: [Void] = []
+ public var selfUserInfo_MockMethod: (() async -> (id: UUID, clientId: String?))?
+ public var selfUserInfo_MockValue: (id: UUID, clientId: String?)?
- if let mock = fetchSelfUser_MockMethod {
- return mock()
- } else if let mock = fetchSelfUser_MockValue {
+ public func selfUserInfo() async -> (id: UUID, clientId: String?) {
+ selfUserInfo_Invocations.append(())
+
+ if let mock = selfUserInfo_MockMethod {
+ return await mock()
+ } else if let mock = selfUserInfo_MockValue {
return mock
} else {
- fatalError("no mock for `fetchSelfUser`")
+ fatalError("no mock for `selfUserInfo`")
}
}
@@ -1049,47 +1475,47 @@ public class MockTeamRepositoryProtocol: TeamRepositoryProtocol {
try await mock()
}
- // MARK: - fetchSelfLegalholdStatus
+ // MARK: - fetchSelfLegalholdInfo
- public var fetchSelfLegalholdStatus_Invocations: [Void] = []
- public var fetchSelfLegalholdStatus_MockError: Error?
- public var fetchSelfLegalholdStatus_MockMethod: (() async throws -> LegalholdStatus)?
- public var fetchSelfLegalholdStatus_MockValue: LegalholdStatus?
+ public var fetchSelfLegalholdInfo_Invocations: [Void] = []
+ public var fetchSelfLegalholdInfo_MockError: Error?
+ public var fetchSelfLegalholdInfo_MockMethod: (() async throws -> TeamMemberLegalholdInfo)?
+ public var fetchSelfLegalholdInfo_MockValue: TeamMemberLegalholdInfo?
- public func fetchSelfLegalholdStatus() async throws -> LegalholdStatus {
- fetchSelfLegalholdStatus_Invocations.append(())
+ public func fetchSelfLegalholdInfo() async throws -> TeamMemberLegalholdInfo {
+ fetchSelfLegalholdInfo_Invocations.append(())
- if let error = fetchSelfLegalholdStatus_MockError {
+ if let error = fetchSelfLegalholdInfo_MockError {
throw error
}
- if let mock = fetchSelfLegalholdStatus_MockMethod {
+ if let mock = fetchSelfLegalholdInfo_MockMethod {
return try await mock()
- } else if let mock = fetchSelfLegalholdStatus_MockValue {
+ } else if let mock = fetchSelfLegalholdInfo_MockValue {
return mock
} else {
- fatalError("no mock for `fetchSelfLegalholdStatus`")
+ fatalError("no mock for `fetchSelfLegalholdInfo`")
}
}
// MARK: - deleteMembership
- public var deleteMembershipForDomainAt_Invocations: [(userID: UUID, domain: String?, time: Date)] = []
- public var deleteMembershipForDomainAt_MockError: Error?
- public var deleteMembershipForDomainAt_MockMethod: ((UUID, String?, Date) async throws -> Void)?
+ public var deleteMembershipUserIDDomainDate_Invocations: [(userID: UUID, domain: String?, date: Date)] = []
+ public var deleteMembershipUserIDDomainDate_MockError: Error?
+ public var deleteMembershipUserIDDomainDate_MockMethod: ((UUID, String?, Date) async throws -> Void)?
- public func deleteMembership(for userID: UUID, domain: String?, at time: Date) async throws {
- deleteMembershipForDomainAt_Invocations.append((userID: userID, domain: domain, time: time))
+ public func deleteMembership(userID: UUID, domain: String?, date: Date) async throws {
+ deleteMembershipUserIDDomainDate_Invocations.append((userID: userID, domain: domain, date: date))
- if let error = deleteMembershipForDomainAt_MockError {
+ if let error = deleteMembershipUserIDDomainDate_MockError {
throw error
}
- guard let mock = deleteMembershipForDomainAt_MockMethod else {
- fatalError("no mock for `deleteMembershipForDomainAt`")
+ guard let mock = deleteMembershipUserIDDomainDate_MockMethod else {
+ fatalError("no mock for `deleteMembershipUserIDDomainDate`")
}
- try await mock(userID, domain, time)
+ try await mock(userID, domain, date)
}
// MARK: - storeTeamMemberNeedsBackendUpdate
@@ -1112,6 +1538,26 @@ public class MockTeamRepositoryProtocol: TeamRepositoryProtocol {
try await mock(membershipID)
}
+ // MARK: - pullSelfLegalholdInfo
+
+ public var pullSelfLegalholdInfo_Invocations: [Void] = []
+ public var pullSelfLegalholdInfo_MockError: Error?
+ public var pullSelfLegalholdInfo_MockMethod: (() async throws -> Void)?
+
+ public func pullSelfLegalholdInfo() async throws {
+ pullSelfLegalholdInfo_Invocations.append(())
+
+ if let error = pullSelfLegalholdInfo_MockError {
+ throw error
+ }
+
+ guard let mock = pullSelfLegalholdInfo_MockMethod else {
+ fatalError("no mock for `pullSelfLegalholdInfo`")
+ }
+
+ try await mock()
+ }
+
}
class MockUpdateEventDecryptorProtocol: UpdateEventDecryptorProtocol {
@@ -1173,6 +1619,133 @@ class MockUpdateEventProcessorProtocol: UpdateEventProcessorProtocol {
}
+class MockUpdateEventsLocalStoreProtocol: UpdateEventsLocalStoreProtocol {
+
+ // MARK: - Life cycle
+
+
+
+ // MARK: - lastEventID
+
+ var lastEventID_Invocations: [Void] = []
+ var lastEventID_MockMethod: (() -> UUID?)?
+ var lastEventID_MockValue: UUID??
+
+ func lastEventID() -> UUID? {
+ lastEventID_Invocations.append(())
+
+ if let mock = lastEventID_MockMethod {
+ return mock()
+ } else if let mock = lastEventID_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `lastEventID`")
+ }
+ }
+
+ // MARK: - storeLastEventID
+
+ var storeLastEventIDId_Invocations: [UUID] = []
+ var storeLastEventIDId_MockMethod: ((UUID) -> Void)?
+
+ func storeLastEventID(id: UUID) {
+ storeLastEventIDId_Invocations.append(id)
+
+ guard let mock = storeLastEventIDId_MockMethod else {
+ fatalError("no mock for `storeLastEventIDId`")
+ }
+
+ mock(id)
+ }
+
+ // MARK: - indexOfLastEventEnvelope
+
+ var indexOfLastEventEnvelope_Invocations: [Void] = []
+ var indexOfLastEventEnvelope_MockError: Error?
+ var indexOfLastEventEnvelope_MockMethod: (() async throws -> Int64)?
+ var indexOfLastEventEnvelope_MockValue: Int64?
+
+ func indexOfLastEventEnvelope() async throws -> Int64 {
+ indexOfLastEventEnvelope_Invocations.append(())
+
+ if let error = indexOfLastEventEnvelope_MockError {
+ throw error
+ }
+
+ if let mock = indexOfLastEventEnvelope_MockMethod {
+ return try await mock()
+ } else if let mock = indexOfLastEventEnvelope_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `indexOfLastEventEnvelope`")
+ }
+ }
+
+ // MARK: - persistEventEnvelope
+
+ var persistEventEnvelopeIndex_Invocations: [(data: Data, index: Int64)] = []
+ var persistEventEnvelopeIndex_MockError: Error?
+ var persistEventEnvelopeIndex_MockMethod: ((Data, Int64) async throws -> Void)?
+
+ func persistEventEnvelope(_ data: Data, index: Int64) async throws {
+ persistEventEnvelopeIndex_Invocations.append((data: data, index: index))
+
+ if let error = persistEventEnvelopeIndex_MockError {
+ throw error
+ }
+
+ guard let mock = persistEventEnvelopeIndex_MockMethod else {
+ fatalError("no mock for `persistEventEnvelopeIndex`")
+ }
+
+ try await mock(data, index)
+ }
+
+ // MARK: - fetchStoredEventEnvelopePayloads
+
+ var fetchStoredEventEnvelopePayloadsLimit_Invocations: [UInt] = []
+ var fetchStoredEventEnvelopePayloadsLimit_MockError: Error?
+ var fetchStoredEventEnvelopePayloadsLimit_MockMethod: ((UInt) async throws -> [Data])?
+ var fetchStoredEventEnvelopePayloadsLimit_MockValue: [Data]?
+
+ func fetchStoredEventEnvelopePayloads(limit: UInt) async throws -> [Data] {
+ fetchStoredEventEnvelopePayloadsLimit_Invocations.append(limit)
+
+ if let error = fetchStoredEventEnvelopePayloadsLimit_MockError {
+ throw error
+ }
+
+ if let mock = fetchStoredEventEnvelopePayloadsLimit_MockMethod {
+ return try await mock(limit)
+ } else if let mock = fetchStoredEventEnvelopePayloadsLimit_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `fetchStoredEventEnvelopePayloadsLimit`")
+ }
+ }
+
+ // MARK: - deleteNextPendingEvents
+
+ var deleteNextPendingEventsLimit_Invocations: [UInt] = []
+ var deleteNextPendingEventsLimit_MockError: Error?
+ var deleteNextPendingEventsLimit_MockMethod: ((UInt) async throws -> Void)?
+
+ func deleteNextPendingEvents(limit: UInt) async throws {
+ deleteNextPendingEventsLimit_Invocations.append(limit)
+
+ if let error = deleteNextPendingEventsLimit_MockError {
+ throw error
+ }
+
+ guard let mock = deleteNextPendingEventsLimit_MockMethod else {
+ fatalError("no mock for `deleteNextPendingEventsLimit`")
+ }
+
+ try await mock(limit)
+ }
+
+}
+
class MockUpdateEventsRepositoryProtocol: UpdateEventsRepositoryProtocol {
// MARK: - Life cycle
@@ -1317,6 +1890,99 @@ class MockUpdateEventsRepositoryProtocol: UpdateEventsRepositoryProtocol {
}
+public class MockUserClientsLocalStoreProtocol: UserClientsLocalStoreProtocol {
+
+ // MARK: - Life cycle
+
+ public init() {}
+
+
+ // MARK: - fetchOrCreateClient
+
+ public var fetchOrCreateClientId_Invocations: [String] = []
+ public var fetchOrCreateClientId_MockMethod: ((String) async -> (client: WireDataModel.UserClient, isNew: Bool))?
+ public var fetchOrCreateClientId_MockValue: (client: WireDataModel.UserClient, isNew: Bool)?
+
+ public func fetchOrCreateClient(id: String) async -> (client: WireDataModel.UserClient, isNew: Bool) {
+ fetchOrCreateClientId_Invocations.append(id)
+
+ if let mock = fetchOrCreateClientId_MockMethod {
+ return await mock(id)
+ } else if let mock = fetchOrCreateClientId_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `fetchOrCreateClientId`")
+ }
+ }
+
+ // MARK: - deletedSelfClients
+
+ public var deletedSelfClientsNewClients_Invocations: [[String]] = []
+ public var deletedSelfClientsNewClients_MockMethod: (([String]) async -> [String])?
+ public var deletedSelfClientsNewClients_MockValue: [String]?
+
+ public func deletedSelfClients(newClients: [String]) async -> [String] {
+ deletedSelfClientsNewClients_Invocations.append(newClients)
+
+ if let mock = deletedSelfClientsNewClients_MockMethod {
+ return await mock(newClients)
+ } else if let mock = deletedSelfClientsNewClients_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `deletedSelfClientsNewClients`")
+ }
+ }
+
+ // MARK: - deleteClient
+
+ public var deleteClientId_Invocations: [String] = []
+ public var deleteClientId_MockMethod: ((String) async -> Void)?
+
+ public func deleteClient(id: String) async {
+ deleteClientId_Invocations.append(id)
+
+ guard let mock = deleteClientId_MockMethod else {
+ fatalError("no mock for `deleteClientId`")
+ }
+
+ await mock(id)
+ }
+
+ // MARK: - updateClient
+
+ public var updateClientIdIsNewClientUserClientInfo_Invocations: [(id: String, isNewClient: Bool, userClientInfo: UserClientInfo)] = []
+ public var updateClientIdIsNewClientUserClientInfo_MockMethod: ((String, Bool, UserClientInfo) async -> Void)?
+
+ public func updateClient(id: String, isNewClient: Bool, userClientInfo: UserClientInfo) async {
+ updateClientIdIsNewClientUserClientInfo_Invocations.append((id: id, isNewClient: isNewClient, userClientInfo: userClientInfo))
+
+ guard let mock = updateClientIdIsNewClientUserClientInfo_MockMethod else {
+ fatalError("no mock for `updateClientIdIsNewClientUserClientInfo`")
+ }
+
+ await mock(id, isNewClient, userClientInfo)
+ }
+
+ // MARK: - allSelfUserClientsAreActiveMLSClients
+
+ public var allSelfUserClientsAreActiveMLSClients_Invocations: [Void] = []
+ public var allSelfUserClientsAreActiveMLSClients_MockMethod: (() async -> Bool)?
+ public var allSelfUserClientsAreActiveMLSClients_MockValue: Bool?
+
+ public func allSelfUserClientsAreActiveMLSClients() async -> Bool {
+ allSelfUserClientsAreActiveMLSClients_Invocations.append(())
+
+ if let mock = allSelfUserClientsAreActiveMLSClients_MockMethod {
+ return await mock()
+ } else if let mock = allSelfUserClientsAreActiveMLSClients_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `allSelfUserClientsAreActiveMLSClients`")
+ }
+ }
+
+}
+
public class MockUserClientsRepositoryProtocol: UserClientsRepositoryProtocol {
// MARK: - Life cycle
@@ -1346,42 +2012,42 @@ public class MockUserClientsRepositoryProtocol: UserClientsRepositoryProtocol {
// MARK: - fetchOrCreateClient
- public var fetchOrCreateClientWith_Invocations: [String] = []
- public var fetchOrCreateClientWith_MockError: Error?
- public var fetchOrCreateClientWith_MockMethod: ((String) async throws -> (client: WireDataModel.UserClient, isNew: Bool))?
- public var fetchOrCreateClientWith_MockValue: (client: WireDataModel.UserClient, isNew: Bool)?
+ public var fetchOrCreateClientId_Invocations: [String] = []
+ public var fetchOrCreateClientId_MockError: Error?
+ public var fetchOrCreateClientId_MockMethod: ((String) async throws -> (client: WireDataModel.UserClient, isNew: Bool))?
+ public var fetchOrCreateClientId_MockValue: (client: WireDataModel.UserClient, isNew: Bool)?
- public func fetchOrCreateClient(with id: String) async throws -> (client: WireDataModel.UserClient, isNew: Bool) {
- fetchOrCreateClientWith_Invocations.append(id)
+ public func fetchOrCreateClient(id: String) async throws -> (client: WireDataModel.UserClient, isNew: Bool) {
+ fetchOrCreateClientId_Invocations.append(id)
- if let error = fetchOrCreateClientWith_MockError {
+ if let error = fetchOrCreateClientId_MockError {
throw error
}
- if let mock = fetchOrCreateClientWith_MockMethod {
+ if let mock = fetchOrCreateClientId_MockMethod {
return try await mock(id)
- } else if let mock = fetchOrCreateClientWith_MockValue {
+ } else if let mock = fetchOrCreateClientId_MockValue {
return mock
} else {
- fatalError("no mock for `fetchOrCreateClientWith`")
+ fatalError("no mock for `fetchOrCreateClientId`")
}
}
// MARK: - updateClient
- public var updateClientWithFromIsNewClient_Invocations: [(id: String, remoteClient: WireAPI.SelfUserClient, isNewClient: Bool)] = []
- public var updateClientWithFromIsNewClient_MockError: Error?
- public var updateClientWithFromIsNewClient_MockMethod: ((String, WireAPI.SelfUserClient, Bool) async throws -> Void)?
+ public var updateClientIdFromIsNewClient_Invocations: [(id: String, remoteClient: WireAPI.SelfUserClient, isNewClient: Bool)] = []
+ public var updateClientIdFromIsNewClient_MockError: Error?
+ public var updateClientIdFromIsNewClient_MockMethod: ((String, WireAPI.SelfUserClient, Bool) async throws -> Void)?
- public func updateClient(with id: String, from remoteClient: WireAPI.SelfUserClient, isNewClient: Bool) async throws {
- updateClientWithFromIsNewClient_Invocations.append((id: id, remoteClient: remoteClient, isNewClient: isNewClient))
+ public func updateClient(id: String, from remoteClient: WireAPI.SelfUserClient, isNewClient: Bool) async throws {
+ updateClientIdFromIsNewClient_Invocations.append((id: id, remoteClient: remoteClient, isNewClient: isNewClient))
- if let error = updateClientWithFromIsNewClient_MockError {
+ if let error = updateClientIdFromIsNewClient_MockError {
throw error
}
- guard let mock = updateClientWithFromIsNewClient_MockMethod else {
- fatalError("no mock for `updateClientWithFromIsNewClient`")
+ guard let mock = updateClientIdFromIsNewClient_MockMethod else {
+ fatalError("no mock for `updateClientIdFromIsNewClient`")
}
try await mock(id, remoteClient, isNewClient)
@@ -1389,14 +2055,14 @@ public class MockUserClientsRepositoryProtocol: UserClientsRepositoryProtocol {
// MARK: - deleteClient
- public var deleteClientWith_Invocations: [String] = []
- public var deleteClientWith_MockMethod: ((String) async -> Void)?
+ public var deleteClientId_Invocations: [String] = []
+ public var deleteClientId_MockMethod: ((String) async -> Void)?
- public func deleteClient(with id: String) async {
- deleteClientWith_Invocations.append(id)
+ public func deleteClient(id: String) async {
+ deleteClientId_Invocations.append(id)
- guard let mock = deleteClientWith_MockMethod else {
- fatalError("no mock for `deleteClientWith`")
+ guard let mock = deleteClientId_MockMethod else {
+ fatalError("no mock for `deleteClientId`")
}
await mock(id)
@@ -1488,57 +2154,37 @@ public class MockUserLocalStoreProtocol: UserLocalStoreProtocol {
}
}
- // MARK: - deletePushToken
-
- public var deletePushToken_Invocations: [Void] = []
- public var deletePushToken_MockMethod: (() -> Void)?
-
- public func deletePushToken() {
- deletePushToken_Invocations.append(())
-
- guard let mock = deletePushToken_MockMethod else {
- fatalError("no mock for `deletePushToken`")
- }
-
- mock()
- }
-
- // MARK: - fetchOrCreateUserClient
+ // MARK: - fetchOrCreateUsers
- public var fetchOrCreateUserClientId_Invocations: [String] = []
- public var fetchOrCreateUserClientId_MockMethod: ((String) async -> (client: WireDataModel.UserClient, isNew: Bool))?
- public var fetchOrCreateUserClientId_MockValue: (client: WireDataModel.UserClient, isNew: Bool)?
+ public var fetchOrCreateUsersUserIDs_Invocations: [[(id: UUID, domain: String?)]] = []
+ public var fetchOrCreateUsersUserIDs_MockMethod: (([(id: UUID, domain: String?)]) async -> Set)?
+ public var fetchOrCreateUsersUserIDs_MockValue: Set?
- public func fetchOrCreateUserClient(id: String) async -> (client: WireDataModel.UserClient, isNew: Bool) {
- fetchOrCreateUserClientId_Invocations.append(id)
+ public func fetchOrCreateUsers(userIDs: [(id: UUID, domain: String?)]) async -> Set {
+ fetchOrCreateUsersUserIDs_Invocations.append(userIDs)
- if let mock = fetchOrCreateUserClientId_MockMethod {
- return await mock(id)
- } else if let mock = fetchOrCreateUserClientId_MockValue {
+ if let mock = fetchOrCreateUsersUserIDs_MockMethod {
+ return await mock(userIDs)
+ } else if let mock = fetchOrCreateUsersUserIDs_MockValue {
return mock
} else {
- fatalError("no mock for `fetchOrCreateUserClientId`")
+ fatalError("no mock for `fetchOrCreateUsersUserIDs`")
}
}
- // MARK: - updateUserClient
-
- public var updateUserClientFromIsNewClient_Invocations: [(localClient: WireDataModel.UserClient, remoteClient: WireAPI.SelfUserClient, isNewClient: Bool)] = []
- public var updateUserClientFromIsNewClient_MockError: Error?
- public var updateUserClientFromIsNewClient_MockMethod: ((WireDataModel.UserClient, WireAPI.SelfUserClient, Bool) async throws -> Void)?
+ // MARK: - deletePushToken
- public func updateUserClient(_ localClient: WireDataModel.UserClient, from remoteClient: WireAPI.SelfUserClient, isNewClient: Bool) async throws {
- updateUserClientFromIsNewClient_Invocations.append((localClient: localClient, remoteClient: remoteClient, isNewClient: isNewClient))
+ public var deletePushToken_Invocations: [Void] = []
+ public var deletePushToken_MockMethod: (() -> Void)?
- if let error = updateUserClientFromIsNewClient_MockError {
- throw error
- }
+ public func deletePushToken() {
+ deletePushToken_Invocations.append(())
- guard let mock = updateUserClientFromIsNewClient_MockMethod else {
- fatalError("no mock for `updateUserClientFromIsNewClient`")
+ guard let mock = deletePushToken_MockMethod else {
+ fatalError("no mock for `deletePushToken`")
}
- try await mock(localClient, remoteClient, isNewClient)
+ mock()
}
// MARK: - addSelfLegalHoldRequest
@@ -1664,32 +2310,55 @@ public class MockUserLocalStoreProtocol: UserLocalStoreProtocol {
// MARK: - persistUser
- public var persistUserFrom_Invocations: [WireAPI.User] = []
- public var persistUserFrom_MockMethod: ((WireAPI.User) async -> Void)?
+ public var persistUserUserInfo_Invocations: [NewUserInfo] = []
+ public var persistUserUserInfo_MockMethod: ((NewUserInfo) async -> Void)?
- public func persistUser(from user: WireAPI.User) async {
- persistUserFrom_Invocations.append(user)
+ public func persistUser(userInfo: NewUserInfo) async {
+ persistUserUserInfo_Invocations.append(userInfo)
- guard let mock = persistUserFrom_MockMethod else {
- fatalError("no mock for `persistUserFrom`")
+ guard let mock = persistUserUserInfo_MockMethod else {
+ fatalError("no mock for `persistUserUserInfo`")
}
- await mock(user)
+ await mock(userInfo)
}
// MARK: - updateUser
- public var updateUserFrom_Invocations: [UserUpdateEvent] = []
- public var updateUserFrom_MockMethod: ((UserUpdateEvent) async -> Void)?
+ public var updateUserUserUpdateInfo_Invocations: [UserUpdateInfo] = []
+ public var updateUserUserUpdateInfo_MockMethod: ((UserUpdateInfo) async -> Void)?
- public func updateUser(from event: UserUpdateEvent) async {
- updateUserFrom_Invocations.append(event)
+ public func updateUser(userUpdateInfo: UserUpdateInfo) async {
+ updateUserUserUpdateInfo_Invocations.append(userUpdateInfo)
- guard let mock = updateUserFrom_MockMethod else {
- fatalError("no mock for `updateUserFrom`")
+ guard let mock = updateUserUserUpdateInfo_MockMethod else {
+ fatalError("no mock for `updateUserUserUpdateInfo`")
}
- await mock(event)
+ await mock(userUpdateInfo)
+ }
+
+ // MARK: - fetchAllUserIDsWithOneOnOneConversation
+
+ public var fetchAllUserIDsWithOneOnOneConversation_Invocations: [Void] = []
+ public var fetchAllUserIDsWithOneOnOneConversation_MockError: Error?
+ public var fetchAllUserIDsWithOneOnOneConversation_MockMethod: (() async throws -> [WireDataModel.QualifiedID])?
+ public var fetchAllUserIDsWithOneOnOneConversation_MockValue: [WireDataModel.QualifiedID]?
+
+ public func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] {
+ fetchAllUserIDsWithOneOnOneConversation_Invocations.append(())
+
+ if let error = fetchAllUserIDsWithOneOnOneConversation_MockError {
+ throw error
+ }
+
+ if let mock = fetchAllUserIDsWithOneOnOneConversation_MockMethod {
+ return try await mock()
+ } else if let mock = fetchAllUserIDsWithOneOnOneConversation_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `fetchAllUserIDsWithOneOnOneConversation`")
+ }
}
}
@@ -1701,6 +2370,26 @@ public class MockUserRepositoryProtocol: UserRepositoryProtocol {
public init() {}
+ // MARK: - pullSelfUser
+
+ public var pullSelfUser_Invocations: [Void] = []
+ public var pullSelfUser_MockError: Error?
+ public var pullSelfUser_MockMethod: (() async throws -> Void)?
+
+ public func pullSelfUser() async throws {
+ pullSelfUser_Invocations.append(())
+
+ if let error = pullSelfUser_MockError {
+ throw error
+ }
+
+ guard let mock = pullSelfUser_MockMethod else {
+ fatalError("no mock for `pullSelfUser`")
+ }
+
+ try await mock()
+ }
+
// MARK: - fetchSelfUser
public var fetchSelfUser_Invocations: [Void] = []
@@ -1958,6 +2647,29 @@ public class MockUserRepositoryProtocol: UserRepositoryProtocol {
}
}
+ // MARK: - fetchAllUserIDsWithOneOnOneConversation
+
+ public var fetchAllUserIDsWithOneOnOneConversation_Invocations: [Void] = []
+ public var fetchAllUserIDsWithOneOnOneConversation_MockError: Error?
+ public var fetchAllUserIDsWithOneOnOneConversation_MockMethod: (() async throws -> [WireDataModel.QualifiedID])?
+ public var fetchAllUserIDsWithOneOnOneConversation_MockValue: [WireDataModel.QualifiedID]?
+
+ public func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] {
+ fetchAllUserIDsWithOneOnOneConversation_Invocations.append(())
+
+ if let error = fetchAllUserIDsWithOneOnOneConversation_MockError {
+ throw error
+ }
+
+ if let mock = fetchAllUserIDsWithOneOnOneConversation_MockMethod {
+ return try await mock()
+ } else if let mock = fetchAllUserIDsWithOneOnOneConversation_MockValue {
+ return mock
+ } else {
+ fatalError("no mock for `fetchAllUserIDsWithOneOnOneConversation`")
+ }
+ }
+
}
// swiftlint:enable variable_name
diff --git a/WireDomain/.swiftpm/WireDomainPackage.xctestplan b/WireDomain/Tests/TestPlans/AllTests.xctestplan
similarity index 64%
rename from WireDomain/.swiftpm/WireDomainPackage.xctestplan
rename to WireDomain/Tests/TestPlans/AllTests.xctestplan
index 455d5b6ee2f..444f2ef0e81 100644
--- a/WireDomain/.swiftpm/WireDomainPackage.xctestplan
+++ b/WireDomain/Tests/TestPlans/AllTests.xctestplan
@@ -1,14 +1,19 @@
{
"configurations" : [
{
- "id" : "9847EFE7-0EA9-4F10-ADDE-417328B9D738",
- "name" : "Test Scheme Action",
+ "id" : "F5BC5855-65E0-4C6D-BEC0-97FFF7D13461",
+ "name" : "Configuration 1",
"options" : {
}
}
],
"defaultOptions" : {
+ "commandLineArgumentEntries" : [
+ {
+ "argument" : "-com.apple.CoreData.ConcurrencyDebug 1"
+ }
+ ],
"language" : "en",
"region" : "DE",
"testExecutionOrdering" : "random"
diff --git a/WireDomain/Tests/WireDomainTests/Event Decryption/ProteusMessageDecryptorTests.swift b/WireDomain/Tests/WireDomainTests/Event Decryption/ProteusMessageDecryptorTests.swift
index 53bf889c81a..9b23c1e3ea4 100644
--- a/WireDomain/Tests/WireDomainTests/Event Decryption/ProteusMessageDecryptorTests.swift
+++ b/WireDomain/Tests/WireDomainTests/Event Decryption/ProteusMessageDecryptorTests.swift
@@ -132,41 +132,6 @@ final class ProteusMessageDecryptorTests: XCTestCase {
}
}
- func testItThrowsWhenCiphertextIsTooBig() async throws {
- // Given a message that exceeds the max ciphertext size
- let longMessage = String(repeating: "!", count: 20_000)
- let invalidEvent = Scaffolding.makeEvent(content: .ciphertext(longMessage))
-
- // When
- do {
- _ = try await sut.decryptedEventData(from: invalidEvent)
- XCTFail("expected an error but none was thrown")
- return
- } catch ProteusError.decodeError {
- // Then we got the right error
- } catch {
- XCTFail("unexpected error: \(error)")
- }
- }
-
- func testItThrowsWhenExternalCiphertextIsTooBig() async throws {
- // Given an external message that exceeds the max ciphertext size
- var invalidEvent = Scaffolding.makeEvent(content: .ciphertext("valid message"))
- let longMessage = String(repeating: "!", count: 20_000)
- invalidEvent.externalData = .ciphertext(longMessage)
-
- // When
- do {
- _ = try await sut.decryptedEventData(from: invalidEvent)
- XCTFail("expected an error but none was thrown")
- return
- } catch ProteusError.decodeError {
- // Then we got the right error
- } catch {
- XCTFail("unexpected error: \(error)")
- }
- }
-
func testItDecryptsAnEventFromANewSenderAndUpdatesSecurityLevel() async throws {
// Given
try await context.perform { [context] in
diff --git a/WireDomain/Tests/WireDomainTests/Event Decryption/UpdateEventDecryptorTests.swift b/WireDomain/Tests/WireDomainTests/Event Decryption/UpdateEventDecryptorTests.swift
index 49d2b082dbf..36985156a54 100644
--- a/WireDomain/Tests/WireDomainTests/Event Decryption/UpdateEventDecryptorTests.swift
+++ b/WireDomain/Tests/WireDomainTests/Event Decryption/UpdateEventDecryptorTests.swift
@@ -134,7 +134,7 @@ final class UpdateEventDecryptorTests: XCTestCase {
// Mock
proteusMessageDecryptor.decryptedEventDataFrom_MockMethod = { _ in
- throw ProteusError.invalidSignature
+ throw ProteusService.DecryptionError.failedToDecryptData(.Other(1))
}
// When
@@ -169,7 +169,7 @@ final class UpdateEventDecryptorTests: XCTestCase {
let lastMessage = try XCTUnwrap(conversation.lastMessage as? ZMSystemMessage)
XCTAssertEqual(lastMessage.systemMessageType, .decryptionFailed)
- XCTAssertEqual(lastMessage.decryptionErrorCode?.intValue, ProteusError.invalidSignature.rawValue)
+ XCTAssertEqual(lastMessage.decryptionErrorCode?.intValue, 1)
XCTAssertEqual(lastMessage.serverTimestamp, Scaffolding.timestamp)
XCTAssertEqual(lastMessage.sender, alice)
XCTAssertEqual(lastMessage.clients, [aliceClient])
@@ -189,43 +189,7 @@ final class UpdateEventDecryptorTests: XCTestCase {
// Mock
proteusMessageDecryptor.decryptedEventDataFrom_MockMethod = { _ in
- throw ProteusError.duplicateMessage
- }
-
- // When
- let events = try await sut.decryptEvents(in: envelope)
-
- // Then we skipped over the proteus message.
- XCTAssertEqual(events, [.user(.pushRemove)])
-
- // Then no system message was appended.
- try await context.perform { [context] in
- let conversation = try XCTUnwrap(
- ZMConversation.fetch(
- with: Scaffolding.conversationID.uuid,
- domain: Scaffolding.conversationID.domain,
- in: context
- )
- )
-
- XCTAssertNil(conversation.lastMessage)
- }
- }
-
- func testWhenOutdatedMessageErrorIsThrownThenNoSystemMessageIsAppended() async throws {
- // Given some events.
- let envelope = UpdateEventEnvelope(
- id: UUID(),
- events: [
- .conversation(.proteusMessageAdd(Scaffolding.proteusMessage)),
- .user(.pushRemove)
- ],
- isTransient: false
- )
-
- // Mock
- proteusMessageDecryptor.decryptedEventDataFrom_MockMethod = { _ in
- throw ProteusError.outdatedMessage
+ throw ProteusService.DecryptionError.failedToDecryptData(.DuplicateMessage)
}
// When
diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationMLSWelcomeEventProcessorTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationMLSWelcomeEventProcessorTests.swift
new file mode 100644
index 00000000000..9bfc419f09e
--- /dev/null
+++ b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationMLSWelcomeEventProcessorTests.swift
@@ -0,0 +1,131 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+import WireDataModelSupport
+import WireDomainSupport
+import XCTest
+@testable import WireAPI
+@testable import WireDomain
+
+final class ConversationMLSWelcomeEventProcessorTests: XCTestCase {
+
+ private var sut: ConversationMLSWelcomeEventProcessor!
+ private var modelHelper: ModelHelper!
+ private var coreDataStack: CoreDataStack!
+ private var coreDataStackHelper: CoreDataStackHelper!
+ private var conversationRepository: MockConversationRepositoryProtocol!
+ private var conversationLocalStore: MockConversationLocalStoreProtocol!
+ private var userLocalStore: MockUserLocalStoreProtocol!
+ private var mlsService: MockMLSServiceInterface!
+ private var mlsDecryptionService: MockMLSDecryptionServiceInterface!
+ private var oneOnOneResolver: MockOneOnOneResolverProtocol!
+
+ private var context: NSManagedObjectContext {
+ coreDataStack.syncContext
+ }
+
+ override func setUp() async throws {
+ modelHelper = ModelHelper()
+ coreDataStackHelper = CoreDataStackHelper()
+ coreDataStack = try await coreDataStackHelper.createStack()
+ conversationRepository = MockConversationRepositoryProtocol()
+ conversationLocalStore = MockConversationLocalStoreProtocol()
+ userLocalStore = MockUserLocalStoreProtocol()
+ mlsService = MockMLSServiceInterface()
+ mlsDecryptionService = MockMLSDecryptionServiceInterface()
+ oneOnOneResolver = MockOneOnOneResolverProtocol()
+
+ sut = ConversationMLSWelcomeEventProcessor(
+ conversationRepository: conversationRepository,
+ conversationLocalStore: conversationLocalStore,
+ mlsService: mlsService,
+ mlsDecryptionService: mlsDecryptionService,
+ oneOnOneResolver: oneOnOneResolver
+ )
+ }
+
+ override func tearDown() async throws {
+ modelHelper = nil
+ coreDataStack = nil
+ sut = nil
+ try coreDataStackHelper.cleanupDirectory()
+ coreDataStackHelper = nil
+ conversationRepository = nil
+ conversationLocalStore = nil
+ userLocalStore = nil
+ mlsService = nil
+ mlsDecryptionService = nil
+ oneOnOneResolver = nil
+ }
+
+ // MARK: - Tests
+
+ func testProcessEvent_It_Invokes_Repository_Local_Store_And_MLS_Services_Methods() async throws {
+
+ // Mock
+
+ let conversation = await context.perform { [self] in
+ modelHelper.createGroupConversation(
+ id: Scaffolding.conversationID.uuid,
+ domain: Scaffolding.conversationID.domain,
+ in: context
+ )
+ }
+
+ mlsDecryptionService.processWelcomeMessageWelcomeMessage_MockValue = Scaffolding.mlsGroupID
+ conversationRepository.fetchConversationIdDomain_MockValue = conversation
+ conversationLocalStore.storeMLSConversationEstablishedMlsGroupIDConversation_MockMethod = { _, _ in }
+ conversationLocalStore.updateOrCreateMLSGroupGroupID_MockMethod = { _ in }
+ mlsService.uploadKeyPackagesIfNeeded_MockMethod = {}
+ conversationLocalStore.fetchOtherUserIDInOneOnOneConversationConversation_MockValue = Scaffolding.qualifiedID
+ oneOnOneResolver.resolveOneOnOneConversationWith_MockMethod = { _ in }
+
+ // When
+
+ try await sut.processEvent(Scaffolding.event)
+
+ // Then
+
+ XCTAssertEqual(mlsDecryptionService.processWelcomeMessageWelcomeMessage_Invocations.count, 1)
+ XCTAssertEqual(conversationRepository.fetchConversationIdDomain_Invocations.count, 1)
+ XCTAssertEqual(
+ conversationLocalStore.storeMLSConversationEstablishedMlsGroupIDConversation_Invocations.count,
+ 1
+ )
+ XCTAssertEqual(conversationLocalStore.updateOrCreateMLSGroupGroupID_Invocations.count, 1)
+ XCTAssertEqual(mlsService.uploadKeyPackagesIfNeeded_Invocations.count, 1)
+ XCTAssertEqual(conversationLocalStore.fetchOtherUserIDInOneOnOneConversationConversation_Invocations.count, 1)
+ XCTAssertEqual(oneOnOneResolver.resolveOneOnOneConversationWith_Invocations.count, 1)
+ }
+
+ private enum Scaffolding {
+ static let domain = "domain.com"
+ static let conversationID = ConversationID(uuid: .mockID1, domain: domain)
+ static let senderID = UserID(uuid: .mockID2, domain: domain)
+ static let mlsGroupID = MLSGroupID.random()
+ static let qualifiedID = WireDataModel.QualifiedID(uuid: .mockID1, domain: domain)
+
+ static let event = ConversationMLSWelcomeEvent(
+ conversationID: conversationID,
+ senderID: senderID,
+ welcomeMessage: Data.random().base64EncodedString()
+ )
+ }
+
+}
diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationMessageTimerUpdateEventProcessorTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationMessageTimerUpdateEventProcessorTests.swift
new file mode 100644
index 00000000000..1b96c363d68
--- /dev/null
+++ b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationMessageTimerUpdateEventProcessorTests.swift
@@ -0,0 +1,109 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+import WireDataModelSupport
+import WireDomainSupport
+import XCTest
+@testable import WireAPI
+@testable import WireDomain
+
+final class ConversationMessageTimerUpdateEventProcessorTests: XCTestCase {
+
+ private var sut: ConversationMessageTimerUpdateEventProcessor!
+ private var conversationLocalStore: MockConversationLocalStoreProtocol!
+ private var messageLocalStore: MockMessageLocalStoreProtocol!
+ private var coreDataStack: CoreDataStack!
+ private var coreDataStackHelper: CoreDataStackHelper!
+ private var modelHelper: ModelHelper!
+
+ private var context: NSManagedObjectContext {
+ coreDataStack.syncContext
+ }
+
+ override func setUp() async throws {
+ try await super.setUp()
+
+ modelHelper = ModelHelper()
+ coreDataStackHelper = CoreDataStackHelper()
+ coreDataStack = try await coreDataStackHelper.createStack()
+
+ conversationLocalStore = MockConversationLocalStoreProtocol()
+ messageLocalStore = MockMessageLocalStoreProtocol()
+
+ sut = ConversationMessageTimerUpdateEventProcessor(
+ conversationLocalStore: conversationLocalStore,
+ messageLocalStore: messageLocalStore
+ )
+ }
+
+ override func tearDown() async throws {
+ try await super.tearDown()
+ sut = nil
+ conversationLocalStore = nil
+ messageLocalStore = nil
+ modelHelper = nil
+ coreDataStack = nil
+ try coreDataStackHelper.cleanupDirectory()
+ coreDataStackHelper = nil
+ }
+
+ // MARK: - Tests
+
+ func testProcessEvent_It_Invokes_Repo_Methods() async {
+ // Mock
+
+ let conversation = await context.perform { [self] in
+ return modelHelper.createGroupConversation(in: context)
+ }
+
+ conversationLocalStore.fetchOrCreateConversationIdDomain_MockValue = conversation
+ conversationLocalStore.conversationMessageDestructionTimeout_MockValue = .fiveMinutes
+ conversationLocalStore.storeConversationTimeoutValueFor_MockMethod = { _, _ in }
+ messageLocalStore
+ .addSystemMessageToConversationMessageTypeConversationIDConversationDomain_MockMethod = { _, _, _ in }
+
+ // When
+
+ await sut.processEvent(Scaffolding.event)
+
+ // Then
+
+ XCTAssertEqual(conversationLocalStore.fetchOrCreateConversationIdDomain_Invocations.count, 1)
+ XCTAssertEqual(conversationLocalStore.conversationMessageDestructionTimeout_Invocations.count, 1)
+ XCTAssertEqual(conversationLocalStore.storeConversationTimeoutValueFor_Invocations.count, 1)
+ XCTAssertEqual(
+ messageLocalStore.addSystemMessageToConversationMessageTypeConversationIDConversationDomain_Invocations
+ .count,
+ 1
+ )
+ }
+
+ private enum Scaffolding {
+ static let id = UUID()
+ static let domain = "domain.com"
+ static let event = ConversationMessageTimerUpdateEvent(
+ conversationID: ConversationID(uuid: id, domain: domain),
+ senderID: UserID(uuid: id, domain: domain),
+ timestamp: .now,
+ newTimer: 10_000
+ )
+
+ }
+
+}
diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessorTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessorTests.swift
index 4da9e925ed6..864d47c9020 100644
--- a/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessorTests.swift
+++ b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessorTests.swift
@@ -29,6 +29,7 @@ final class ConversationReceiptModeUpdateEventProcessorTests: XCTestCase {
private var userRepository: MockUserRepositoryProtocol!
private var conversationRepository: MockConversationRepositoryProtocol!
private var conversationLocalStore: MockConversationLocalStoreProtocol!
+ private var messageRepository: MockMessageRepositoryProtocol!
private var coreDataStack: CoreDataStack!
private var coreDataStackHelper: CoreDataStackHelper!
private var modelHelper: ModelHelper!
@@ -38,22 +39,23 @@ final class ConversationReceiptModeUpdateEventProcessorTests: XCTestCase {
}
override func setUp() async throws {
- try await super.setUp()
modelHelper = ModelHelper()
coreDataStackHelper = CoreDataStackHelper()
coreDataStack = try await coreDataStackHelper.createStack()
userRepository = MockUserRepositoryProtocol()
conversationRepository = MockConversationRepositoryProtocol()
conversationLocalStore = MockConversationLocalStoreProtocol()
+ messageRepository = MockMessageRepositoryProtocol()
+
sut = ConversationReceiptModeUpdateEventProcessor(
userRepository: userRepository,
conversationRepository: conversationRepository,
- conversationLocalStore: conversationLocalStore
+ conversationLocalStore: conversationLocalStore,
+ messageRepository: messageRepository
)
}
override func tearDown() async throws {
- try await super.tearDown()
modelHelper = nil
coreDataStack = nil
userRepository = nil
@@ -62,6 +64,7 @@ final class ConversationReceiptModeUpdateEventProcessorTests: XCTestCase {
sut = nil
try coreDataStackHelper.cleanupDirectory()
coreDataStackHelper = nil
+ messageRepository = nil
}
// MARK: - Tests
@@ -78,11 +81,9 @@ final class ConversationReceiptModeUpdateEventProcessorTests: XCTestCase {
userRepository.fetchUserIdDomain_MockValue = user
conversationRepository.fetchConversationIdDomain_MockValue = conversation
- conversationRepository.addSystemMessageTo_MockMethod = { _, _ in }
+ messageRepository
+ .addMessageToConversationMessageTypeConversationIDConversationDomain_MockMethod = { _, _, _ in }
conversationLocalStore.storeConversationHasReadReceiptsEnabledFor_MockMethod = { _, _ in }
- conversationLocalStore.isConversationArchived_MockValue = true
- conversationLocalStore.conversationMutedMessageTypes_MockValue = MutedMessageTypes.none
- conversationLocalStore.storeConversationIsArchivedFor_MockMethod = { _, _ in }
// When
@@ -92,11 +93,11 @@ final class ConversationReceiptModeUpdateEventProcessorTests: XCTestCase {
XCTAssertEqual(userRepository.fetchUserIdDomain_Invocations.count, 1)
XCTAssertEqual(conversationRepository.fetchConversationIdDomain_Invocations.count, 1)
- XCTAssertEqual(conversationRepository.addSystemMessageTo_Invocations.count, 1)
+ XCTAssertEqual(
+ messageRepository.addMessageToConversationMessageTypeConversationIDConversationDomain_Invocations.count,
+ 1
+ )
XCTAssertEqual(conversationLocalStore.storeConversationHasReadReceiptsEnabledFor_Invocations.count, 1)
- XCTAssertEqual(conversationLocalStore.isConversationArchived_Invocations.count, 1)
- XCTAssertEqual(conversationLocalStore.conversationMutedMessageTypes_Invocations.count, 1)
- XCTAssertEqual(conversationLocalStore.storeConversationIsArchivedFor_Invocations.count, 1)
}
private enum Scaffolding {
@@ -105,7 +106,7 @@ final class ConversationReceiptModeUpdateEventProcessorTests: XCTestCase {
static let event = ConversationReceiptModeUpdateEvent(
conversationID: ConversationID(uuid: id, domain: domain),
senderID: UserID(uuid: id, domain: domain),
- newRecieptMode: 1
+ newReceiptMode: 1
)
}
}
diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationRenameEventProcessorTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationRenameEventProcessorTests.swift
new file mode 100644
index 00000000000..42443cc0cc7
--- /dev/null
+++ b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationRenameEventProcessorTests.swift
@@ -0,0 +1,76 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDomainSupport
+import XCTest
+@testable import WireAPI
+@testable import WireDomain
+
+final class ConversationRenameEventProcessorTests: XCTestCase {
+
+ private var sut: ConversationRenameEventProcessor!
+ private var conversationRepository: MockConversationRepositoryProtocol!
+
+ override func setUp() async throws {
+ conversationRepository = MockConversationRepositoryProtocol()
+ sut = ConversationRenameEventProcessor(
+ repository: conversationRepository
+ )
+ }
+
+ override func tearDown() async throws {
+ conversationRepository = nil
+ sut = nil
+ }
+
+ // MARK: - Tests
+
+ func testProcessEvent_It_Invokes_Repo_And_Local_Store_Methods() async throws {
+
+ // Mock
+
+ conversationRepository
+ .updateConversationNameNewNameConversationIDConversationDomainSenderIDSenderDomainDate_MockMethod =
+ { _, _, _, _, _, _ in }
+
+ // When
+
+ try await sut.processEvent(Scaffolding.event)
+
+ // Then
+
+ XCTAssertEqual(
+ conversationRepository
+ .updateConversationNameNewNameConversationIDConversationDomainSenderIDSenderDomainDate_Invocations
+ .count,
+ 1
+ )
+ }
+
+ private enum Scaffolding {
+ static let id = UUID.mockID1
+ static let domain = "domain.com"
+
+ static let event = ConversationRenameEvent(
+ conversationID: ConversationID(uuid: id, domain: domain),
+ senderID: UserID(uuid: id, domain: domain),
+ timestamp: .now,
+ newName: "New conversation name"
+ )
+ }
+}
diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessorTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessorTests.swift
new file mode 100644
index 00000000000..5f95e8dc42b
--- /dev/null
+++ b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationTypingEventProcessorTests.swift
@@ -0,0 +1,105 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+import WireDataModelSupport
+import WireDomainSupport
+import XCTest
+@testable import WireAPI
+@testable import WireDomain
+
+final class ConversationTypingEventProcessorTests: XCTestCase {
+
+ private var sut: ConversationTypingEventProcessor!
+ private var userRepository: MockUserRepositoryProtocol!
+ private var conversationRepository: MockConversationRepositoryProtocol!
+ private var conversationLocalStore: MockConversationLocalStoreProtocol!
+ private var coreDataStack: CoreDataStack!
+ private var coreDataStackHelper: CoreDataStackHelper!
+ private var modelHelper: ModelHelper!
+
+ private var context: NSManagedObjectContext {
+ coreDataStack.syncContext
+ }
+
+ override func setUp() async throws {
+ modelHelper = ModelHelper()
+ coreDataStackHelper = CoreDataStackHelper()
+ coreDataStack = try await coreDataStackHelper.createStack()
+ userRepository = MockUserRepositoryProtocol()
+ conversationRepository = MockConversationRepositoryProtocol()
+ conversationLocalStore = MockConversationLocalStoreProtocol()
+
+ sut = ConversationTypingEventProcessor(
+ conversationRepository: conversationRepository,
+ conversationLocalStore: conversationLocalStore,
+ userRepository: userRepository
+ )
+ }
+
+ override func tearDown() async throws {
+ modelHelper = nil
+ coreDataStack = nil
+ userRepository = nil
+ conversationRepository = nil
+ conversationLocalStore = nil
+ sut = nil
+ try coreDataStackHelper.cleanupDirectory()
+ coreDataStackHelper = nil
+ }
+
+ // MARK: - Tests
+
+ func testProcessEvent_It_Invokes_Repo_And_Local_Store_Methods() async throws {
+
+ // Mock
+
+ let (user, conversation) = await context.perform { [self] in
+ let user = modelHelper.createUser(in: context)
+ let conversation = modelHelper.createGroupConversation(in: context)
+
+ return (user, conversation)
+ }
+
+ userRepository.fetchOrCreateUserIdDomain_MockValue = user
+ conversationRepository.fetchOrCreateConversationIdDomain_MockValue = conversation
+ conversationRepository.updateTypingUsers_MockMethod = { _ in }
+ conversationLocalStore.obtainPermanentIDsUserConversation_MockMethod = { _, _ in }
+
+ // When
+
+ await sut.processEvent(Scaffolding.event)
+
+ // Then
+
+ XCTAssertEqual(conversationLocalStore.obtainPermanentIDsUserConversation_Invocations.count, 1)
+ XCTAssertEqual(userRepository.fetchOrCreateUserIdDomain_Invocations.count, 1)
+ XCTAssertEqual(conversationRepository.fetchOrCreateConversationIdDomain_Invocations.count, 1)
+ XCTAssertEqual(conversationRepository.updateTypingUsers_Invocations.count, 1)
+ }
+
+ private enum Scaffolding {
+ static let id = UUID()
+ static let domain = "domain.com"
+ static let event = ConversationTypingEvent(
+ conversationID: ConversationID(uuid: id, domain: domain),
+ senderID: UserID(uuid: id, domain: domain),
+ isTyping: true
+ )
+ }
+}
diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationTypingUsersTimeoutTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationTypingUsersTimeoutTests.swift
new file mode 100644
index 00000000000..aa5f7d964d5
--- /dev/null
+++ b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationTypingUsersTimeoutTests.swift
@@ -0,0 +1,297 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+import WireDataModelSupport
+import XCTest
+@testable import WireDomain
+
+final class ConversationTypingUsersTimeoutTests: XCTestCase {
+
+ private var sut: ConversationTypingUsersTimeout!
+ private var coreDataStack: CoreDataStack!
+ private var coreDataStackHelper: CoreDataStackHelper!
+ private var modelHelper: ModelHelper!
+
+ private var context: NSManagedObjectContext {
+ coreDataStack.syncContext
+ }
+
+ private var conversationAObjectID: NSManagedObjectID!
+ private var conversationBObjectID: NSManagedObjectID!
+ private var userAObjectID: NSManagedObjectID!
+ private var userBObjectID: NSManagedObjectID!
+
+ override func setUp() async throws {
+ modelHelper = ModelHelper()
+ coreDataStackHelper = CoreDataStackHelper()
+ coreDataStack = try await coreDataStackHelper.createStack()
+
+ sut = ConversationTypingUsersTimeout()
+
+ await setupObjectIDs()
+ }
+
+ override func tearDown() async throws {
+ sut = nil
+ modelHelper = nil
+ coreDataStack = nil
+ try coreDataStackHelper.cleanupDirectory()
+ coreDataStackHelper = nil
+ conversationAObjectID = nil
+ conversationBObjectID = nil
+ userAObjectID = nil
+ userBObjectID = nil
+ }
+
+ // MARK: - Adding / Removing Users
+
+ func testContains_It_Does_Not_Contain_Users_That_We_Have_Not_Added() {
+
+ // Given, then
+
+ XCTAssertFalse(sut.contains(userAObjectID, for: conversationAObjectID))
+ XCTAssertFalse(sut.contains(userBObjectID, for: conversationBObjectID))
+ }
+
+ func testContains_It_Can_Add_A_User() {
+
+ // When
+
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: Date())
+
+ // Then
+
+ XCTAssertTrue(sut.contains(userAObjectID, for: conversationAObjectID))
+ }
+
+ func testRemove_It_Can_Remove_A_User() {
+
+ // Given
+
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: Date())
+ sut.add(userBObjectID, for: conversationAObjectID, withTimeout: Date())
+
+ // When
+
+ sut.remove(userAObjectID, for: conversationAObjectID)
+
+ // Then
+
+ XCTAssertFalse(sut.contains(userAObjectID, for: conversationAObjectID))
+ XCTAssertTrue(sut.contains(userBObjectID, for: conversationAObjectID))
+ }
+
+ func test_That_First_Timeout_Is_Nil_If_Timeouts_Is_Empty() {
+
+ // Given, Then
+
+ XCTAssertNil(sut.firstTimeout)
+ }
+
+ func testRemove_First_Timeout_Is_Nil_For_Users_Added_And_Removed_Again() {
+
+ // Given
+
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: Date())
+
+ // When
+
+ sut.remove(userAObjectID, for: conversationAObjectID)
+
+ // Then
+
+ XCTAssertNil(sut.firstTimeout)
+ }
+
+ func testThatItReturnsTheTimeoutWhenAUserIsAdded() {
+ // Given
+ let timeout = Date()
+
+ // When
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: timeout)
+
+ // Then
+ XCTAssertEqual(sut.firstTimeout, timeout)
+ }
+
+ func testAdd_It_Returns_The_Earliest_Timeout_When_Multiple_Are_Added() {
+
+ // Given
+
+ let timeout1 = Date()
+ let timeout2 = timeout1.addingTimeInterval(10)
+ let timeout3 = timeout2.addingTimeInterval(20)
+
+ // When
+
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: timeout1)
+ sut.add(userAObjectID, for: conversationBObjectID, withTimeout: timeout2)
+ sut.add(userBObjectID, for: conversationAObjectID, withTimeout: timeout3)
+
+ // Then
+
+ XCTAssertEqual(sut.firstTimeout, timeout1)
+ }
+
+ func testAdd_It_Returns_The_Last_Set_Timeout_When_Added_Multiple_Times_For_The_Same_User_And_Conversation() {
+
+ // Given
+
+ let timeout1 = Date()
+ let timeout2 = timeout1.addingTimeInterval(10)
+ let timeout3 = timeout2.addingTimeInterval(20)
+
+ // When
+
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: timeout1)
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: timeout2)
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: timeout3)
+
+ // Then
+
+ XCTAssertEqual(sut.firstTimeout, timeout3)
+ }
+
+ func testAdd_It_Returns_The_Currently_Typing_User_Ids() {
+
+ // Given
+
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: Date())
+ sut.add(userBObjectID, for: conversationAObjectID, withTimeout: Date())
+
+ // When
+
+ let result = sut.userIds(in: conversationAObjectID)
+
+ // Then
+
+ XCTAssertEqual(result, Set([userAObjectID, userBObjectID]))
+ }
+
+ func testUserIds_It_Returns_An_Empty_Set_When_No_Users_Are_Typing() {
+
+ // When
+
+ let result = sut.userIds(in: conversationAObjectID)
+
+ // Then
+
+ XCTAssertEqual(result, Set())
+ }
+
+ func testPruneConversations_It_Returns_An_Empty_Set_When_Pruning_And_Nothing_Was_Added() {
+ // When
+ let result = sut.pruneConversationsThatHaveTimoutBefore(date: Date(timeIntervalSinceNow: -10))
+
+ // Then
+ XCTAssertEqual(result, Set())
+ }
+
+ func testPruneConversations_It_Returns_An_Empty_Set_When_Pruning_And_Nothing_Has_Expired() {
+
+ // Given
+
+ let timeout = Date(timeIntervalSinceNow: 10)
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: timeout)
+
+ // When
+
+ let result = sut.pruneConversationsThatHaveTimoutBefore(date: Date())
+
+ // Then
+
+ XCTAssertEqual(result, Set())
+ }
+
+ func testPruneConversationsIt_Returns_A_Pruned_Conversation() {
+
+ // Given
+
+ let timeout1 = Date(timeIntervalSinceNow: 10)
+ let timeout2 = Date(timeIntervalSinceNow: 20)
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: timeout1)
+
+ // When
+
+ let result = sut.pruneConversationsThatHaveTimoutBefore(date: timeout2)
+
+ // Then
+
+ XCTAssertEqual(result, Set([conversationAObjectID]))
+ }
+
+ func testPruneConversations_It_Returns_Multiple_Pruned_Conversations() {
+
+ // Given
+
+ let timeout1 = Date(timeIntervalSinceNow: 10)
+ let timeout2 = Date(timeIntervalSinceNow: 20)
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: timeout1)
+ sut.add(userBObjectID, for: conversationBObjectID, withTimeout: timeout1)
+
+ // When
+
+ let result = sut.pruneConversationsThatHaveTimoutBefore(date: timeout2)
+
+ // Then
+
+ XCTAssertEqual(result, Set([conversationAObjectID, conversationBObjectID]))
+ }
+
+ func testPruneConversations_It_Removes_Users_When_Pruning() {
+
+ // Given
+
+ let timeout1 = Date(timeIntervalSinceNow: 10)
+ let timeout2 = Date(timeIntervalSinceNow: 15)
+ let timeout3 = Date(timeIntervalSinceNow: 20)
+ sut.add(userAObjectID, for: conversationAObjectID, withTimeout: timeout1)
+ sut.add(userBObjectID, for: conversationAObjectID, withTimeout: timeout3)
+
+ // When
+
+ _ = sut.pruneConversationsThatHaveTimoutBefore(date: timeout2)
+
+ // Then
+
+ XCTAssertFalse(sut.contains(userAObjectID, for: conversationAObjectID))
+ XCTAssertTrue(sut.contains(userBObjectID, for: conversationAObjectID))
+ XCTAssertEqual(sut.userIds(in: conversationAObjectID), Set([userBObjectID]))
+ }
+
+ private func setupObjectIDs() async {
+ await context.perform { [self] in
+ conversationAObjectID = modelHelper.createGroupConversation(
+ in: context
+ ).objectID
+
+ conversationBObjectID = modelHelper.createGroupConversation(
+ in: context
+ ).objectID
+
+ userAObjectID = modelHelper.createUser(
+ in: context
+ ).objectID
+
+ userBObjectID = modelHelper.createUser(
+ in: context
+ ).objectID
+ }
+ }
+}
diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/FeatureConfigEventProcessor/FeatureConfigUpdateEventProcessorTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/FeatureConfigEventProcessor/FeatureConfigUpdateEventProcessorTests.swift
index 4bb115e0ed8..b3e7c2e0312 100644
--- a/WireDomain/Tests/WireDomainTests/Event Processing/FeatureConfigEventProcessor/FeatureConfigUpdateEventProcessorTests.swift
+++ b/WireDomain/Tests/WireDomainTests/Event Processing/FeatureConfigEventProcessor/FeatureConfigUpdateEventProcessorTests.swift
@@ -19,6 +19,7 @@
import WireAPI
import WireDomainSupport
import XCTest
+
@testable import WireDomain
final class FeatureConfigUpdateEventProcessorTests: XCTestCase {
diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessorTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessorTests.swift
index 8cdd158eaa5..63ec1f7d2c8 100644
--- a/WireDomain/Tests/WireDomainTests/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessorTests.swift
+++ b/WireDomain/Tests/WireDomainTests/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessorTests.swift
@@ -45,7 +45,7 @@ final class TeamMemberLeaveEventProcessorTests: XCTestCase {
func testProcessEvent_It_Invokes_Delete_Team_Membership_Repo_Method() async throws {
// Mock
- teamRepository.deleteMembershipForDomainAt_MockMethod = { _, _, _ in }
+ teamRepository.deleteMembershipUserIDDomainDate_MockMethod = { _, _, _ in }
// When
@@ -53,7 +53,7 @@ final class TeamMemberLeaveEventProcessorTests: XCTestCase {
// Then
- XCTAssertEqual(teamRepository.deleteMembershipForDomainAt_Invocations.count, 1)
+ XCTAssertEqual(teamRepository.deleteMembershipUserIDDomainDate_Invocations.count, 1)
}
private enum Scaffolding {
diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/UserClientAddEventProcessorTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/UserClientAddEventProcessorTests.swift
index a82eb09788d..51d4a6cf656 100644
--- a/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/UserClientAddEventProcessorTests.swift
+++ b/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/UserClientAddEventProcessorTests.swift
@@ -66,11 +66,11 @@ final class UserClientAddEventProcessorTests: XCTestCase {
modelHelper.createSelfClient(in: context)
}
- userClientsRepository.fetchOrCreateClientWith_MockMethod = { _ in
+ userClientsRepository.fetchOrCreateClientId_MockMethod = { _ in
(userClient, true)
}
- userClientsRepository.updateClientWithFromIsNewClient_MockMethod = { _, _, _ in }
+ userClientsRepository.updateClientIdFromIsNewClient_MockMethod = { _, _, _ in }
// When
@@ -78,8 +78,8 @@ final class UserClientAddEventProcessorTests: XCTestCase {
// Then
- XCTAssertEqual(userClientsRepository.fetchOrCreateClientWith_Invocations.count, 1)
- XCTAssertEqual(userClientsRepository.updateClientWithFromIsNewClient_Invocations.count, 1)
+ XCTAssertEqual(userClientsRepository.fetchOrCreateClientId_Invocations.count, 1)
+ XCTAssertEqual(userClientsRepository.updateClientIdFromIsNewClient_Invocations.count, 1)
}
private enum Scaffolding {
diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/UserConnectionEventProcessorTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/UserConnectionEventProcessorTests.swift
index 9f63b2bbfc4..0dbe51b5392 100644
--- a/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/UserConnectionEventProcessorTests.swift
+++ b/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/UserConnectionEventProcessorTests.swift
@@ -57,7 +57,7 @@ final class UserConnectionEventProcessorTests: XCTestCase {
// Mock
connectionsRepository.updateConnection_MockMethod = { _ in }
- oneOnOneResolver.invoke_MockMethod = {}
+ oneOnOneResolver.resolveAllOneOnOneConversations_MockMethod = {}
// When
@@ -66,7 +66,7 @@ final class UserConnectionEventProcessorTests: XCTestCase {
// Then
XCTAssertEqual(connectionsRepository.updateConnection_Invocations, [event.connection])
- XCTAssertEqual(oneOnOneResolver.invoke_Invocations.count, 1)
+ XCTAssertEqual(oneOnOneResolver.resolveAllOneOnOneConversations_Invocations.count, 1)
}
private enum Scaffolding {
diff --git a/WireDomain/Tests/WireDomainTests/LocalStores/ConnectionsLocalStoreTests.swift b/WireDomain/Tests/WireDomainTests/LocalStores/ConnectionsLocalStoreTests.swift
new file mode 100644
index 00000000000..c53178835bd
--- /dev/null
+++ b/WireDomain/Tests/WireDomainTests/LocalStores/ConnectionsLocalStoreTests.swift
@@ -0,0 +1,175 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModel
+import WireDataModelSupport
+import XCTest
+@testable import WireDomain
+
+final class ConnectionsLocalStoreTests: XCTestCase {
+
+ private var sut: ConnectionsLocalStore!
+ private var stack: CoreDataStack!
+ private var coreDataStackHelper: CoreDataStackHelper!
+ private var modelHelper: ModelHelper!
+
+ private var context: NSManagedObjectContext {
+ stack.syncContext
+ }
+
+ override func setUp() async throws {
+ BackendInfo.isFederationEnabled = false
+
+ modelHelper = ModelHelper()
+ coreDataStackHelper = CoreDataStackHelper()
+ stack = try await coreDataStackHelper.createStack()
+ sut = ConnectionsLocalStore(context: context)
+ }
+
+ override func tearDown() async throws {
+ stack = nil
+ sut = nil
+ try coreDataStackHelper.cleanupDirectory()
+ modelHelper = nil
+ coreDataStackHelper = nil
+ }
+
+ // MARK: - Tests
+
+ func testPullConnections_GivenConnectionDoesNotExist_FederationDisabled() async throws {
+ try await internalTestPullConnections_GivenConnectionDoesNotExist(
+ federationEnabled: false
+ )
+ }
+
+ func testPullConnections_GivenConnectionDoesNotExist_FederationEnabled() async throws {
+ BackendInfo.isFederationEnabled = true
+ try await internalTestPullConnections_GivenConnectionDoesNotExist(
+ federationEnabled: true
+ )
+ }
+
+ // MARK: Private
+
+ func internalTestPullConnections_GivenConnectionDoesNotExist(
+ federationEnabled: Bool,
+ file: StaticString = #file,
+ line: UInt = #line
+ ) async throws {
+ // Mock
+
+ let connection = Scaffolding.connection
+
+ // When
+
+ try await sut.storeConnection(connection)
+
+ // Then
+
+ try await context.perform { [context] in
+ // There is a connection in the database.
+ let storedConnection = try XCTUnwrap(ZMConnection.fetch(
+ userID: Scaffolding.member2ID.uuid,
+ domain: Scaffolding.member2ID.domain,
+ in: context
+ ))
+
+ XCTAssertEqual(storedConnection.lastUpdateDateInGMT, connection.lastUpdate)
+
+ XCTAssertEqual(storedConnection.to.remoteIdentifier, connection.receiverID)
+ if federationEnabled {
+ XCTAssertEqual(storedConnection.to.domain, connection.receiverQualifiedID?.domain)
+ } else {
+ XCTAssertNil(storedConnection.to.domain)
+ }
+ XCTAssertEqual(storedConnection.status, ZMConnectionStatus.accepted)
+
+ let relatedConversation = try XCTUnwrap(storedConnection.to.oneOnOneConversation)
+ XCTAssertEqual(relatedConversation.remoteIdentifier, connection.qualifiedConversationID?.uuid)
+
+ if federationEnabled {
+ XCTAssertEqual(relatedConversation.domain, connection.qualifiedConversationID?.domain)
+ } else {
+ XCTAssertNil(relatedConversation.domain)
+ }
+
+ XCTAssertTrue(relatedConversation.needsToBeUpdatedFromBackend)
+ }
+ }
+
+ func testUpdateConnection_It_Successfully_Updates_Connection_Locally() async throws {
+ // Given
+
+ let connection = Scaffolding.connection
+
+ // When
+
+ try await sut.storeConnection(connection)
+
+ // Then
+
+ try await context.perform { [context] in
+ let storedConnection = try XCTUnwrap(ZMConnection.fetch(
+ userID: Scaffolding.member2ID.uuid,
+ domain: Scaffolding.member2ID.domain,
+ in: context
+ ))
+
+ XCTAssertEqual(storedConnection.lastUpdateDateInGMT, connection.lastUpdate)
+
+ XCTAssertEqual(storedConnection.to.remoteIdentifier, connection.receiverID)
+ XCTAssertNil(storedConnection.to.domain)
+ XCTAssertEqual(storedConnection.status, ZMConnectionStatus.accepted)
+
+ let relatedConversation = try XCTUnwrap(storedConnection.to.oneOnOneConversation)
+ XCTAssertEqual(relatedConversation.remoteIdentifier, connection.qualifiedConversationID?.uuid)
+
+ XCTAssertNil(relatedConversation.domain)
+
+ XCTAssertTrue(relatedConversation.needsToBeUpdatedFromBackend)
+ }
+ }
+
+ private enum Scaffolding {
+ nonisolated(unsafe) static let member1ID = WireDataModel.QualifiedID(
+ uuid: .mockID1,
+ domain: String.randomDomain()
+ )
+ nonisolated(unsafe) static let conversationID = WireDataModel.QualifiedID(
+ uuid: .mockID2,
+ domain: String.randomDomain()
+ )
+ nonisolated(unsafe) static let member2ID = WireDataModel.QualifiedID(
+ uuid: .mockID3,
+ domain: String.randomDomain()
+ )
+ static let lastUpdate = Date()
+ static let connectionStatus = ZMConnectionStatus.accepted
+
+ static let connection = ConnectionInfo(
+ senderID: Scaffolding.member1ID.uuid,
+ receiverID: Scaffolding.member2ID.uuid,
+ receiverQualifiedID: Scaffolding.member2ID,
+ conversationID: Scaffolding.conversationID.uuid,
+ qualifiedConversationID: Scaffolding.conversationID,
+ lastUpdate: Scaffolding.lastUpdate,
+ status: Scaffolding.connectionStatus
+ )
+ }
+
+}
diff --git a/WireDomain/Tests/WireDomainTests/LocalStores/ConversationLabelsLocalStoreTests.swift b/WireDomain/Tests/WireDomainTests/LocalStores/ConversationLabelsLocalStoreTests.swift
new file mode 100644
index 00000000000..facb43d1475
--- /dev/null
+++ b/WireDomain/Tests/WireDomainTests/LocalStores/ConversationLabelsLocalStoreTests.swift
@@ -0,0 +1,310 @@
+//
+// Wire
+// Copyright (C) 2024 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import WireDataModelSupport
+import WireTestingPackage
+import XCTest
+@testable import WireDataModel
+@testable import WireDomain
+
+final class ConversationLabelsLocalStoreTests: XCTestCase {
+
+ private var sut: ConversationLabelsLocalStore!
+ private var stack: CoreDataStack!
+ private var coreDataStackHelper: CoreDataStackHelper!
+ private var modelHelper: ModelHelper!
+
+ private var conversation1: ZMConversation!
+ private var conversation2: ZMConversation!
+ private var conversation3: ZMConversation!
+
+ private var context: NSManagedObjectContext {
+ stack.syncContext
+ }
+
+ override func setUp() async throws {
+ modelHelper = ModelHelper()
+ coreDataStackHelper = CoreDataStackHelper()
+ /// Batch requests don't work with in-memory store
+ /// so we need to use a persistent store.
+ stack = try await coreDataStackHelper.createStack(inMemoryStore: false)
+ await cleanUpEntity()
+ await setupConversations()
+
+ sut = ConversationLabelsLocalStore(
+ context: context
+ )
+ }
+
+ override func tearDown() async throws {
+ try coreDataStackHelper.cleanupDirectory()
+ coreDataStackHelper = nil
+ sut = nil
+ modelHelper = nil
+ }
+
+ // MARK: - Tests
+
+ func testStoreLabel_Given_Local_Store_Empty_It_Creates_Label_Locally() async throws {
+ // Mock
+
+ let conversationLabel = Scaffolding.conversationLabel1
+
+ // When
+
+ try await sut.storeLabel(conversationLabel)
+
+ // Then
+
+ try await context.perform { [context] in
+ let fetchRequest = NSFetchRequest