diff --git a/Authenticator/Application/Support/Settings.bundle/Acknowledgements.plist b/Authenticator/Application/Support/Settings.bundle/Acknowledgements.plist
index ce20c864fc..bfb66d92d7 100644
--- a/Authenticator/Application/Support/Settings.bundle/Acknowledgements.plist
+++ b/Authenticator/Application/Support/Settings.bundle/Acknowledgements.plist
@@ -122,6 +122,14 @@
Type
PSChildPaneSpecifier
+
+ File
+ Acknowledgements/Sourcery
+ Title
+ Sourcery
+ Type
+ PSChildPaneSpecifier
+
File
Acknowledgements/swift-custom-dump
diff --git a/Authenticator/Application/Support/Settings.bundle/Acknowledgements/Sourcery.plist b/Authenticator/Application/Support/Settings.bundle/Acknowledgements/Sourcery.plist
new file mode 100644
index 0000000000..51edcbca1b
--- /dev/null
+++ b/Authenticator/Application/Support/Settings.bundle/Acknowledgements/Sourcery.plist
@@ -0,0 +1,38 @@
+
+
+
+
+ PreferenceSpecifiers
+
+
+ FooterText
+ MIT License
+
+Copyright (c) 2016-2021 Krzysztof Zabłocki
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+ License
+ MIT
+ Type
+ PSGroupSpecifier
+
+
+
+
diff --git a/AuthenticatorShared/Sourcery/Generated/.gitignore b/AuthenticatorShared/Sourcery/Generated/.gitignore
new file mode 100644
index 0000000000..5a0e94031c
--- /dev/null
+++ b/AuthenticatorShared/Sourcery/Generated/.gitignore
@@ -0,0 +1,2 @@
+# Ignore SwiftGen generated files
+*.swift
diff --git a/AuthenticatorShared/Sourcery/sourcery.yml b/AuthenticatorShared/Sourcery/sourcery.yml
new file mode 100644
index 0000000000..f8d6adb8c3
--- /dev/null
+++ b/AuthenticatorShared/Sourcery/sourcery.yml
@@ -0,0 +1,18 @@
+sources:
+ - ..
+
+templates:
+ - ../../Sourcery/Templates/AutoMockable.stencil
+
+output:
+ Generated
+
+exclude:
+ - Generated
+ - Tests
+ - TestHelpers
+ - Fixtures
+
+args:
+ autoMockableImports: ["BitwardenKit", "BitwardenSdk", "Combine"]
+ autoMockableTestableImports: ["AuthenticatorShared"]
\ No newline at end of file
diff --git a/BitwardenShared/Sourcery/Generated/.gitignore b/BitwardenShared/Sourcery/Generated/.gitignore
new file mode 100644
index 0000000000..5a0e94031c
--- /dev/null
+++ b/BitwardenShared/Sourcery/Generated/.gitignore
@@ -0,0 +1,2 @@
+# Ignore SwiftGen generated files
+*.swift
diff --git a/BitwardenShared/Sourcery/sourcery.yml b/BitwardenShared/Sourcery/sourcery.yml
new file mode 100644
index 0000000000..5e4c42c2d1
--- /dev/null
+++ b/BitwardenShared/Sourcery/sourcery.yml
@@ -0,0 +1,18 @@
+sources:
+ - ..
+
+templates:
+ - ../../Sourcery/Templates/AutoMockable.stencil
+
+output:
+ Generated
+
+exclude:
+ - Generated
+ - Tests
+ - TestHelpers
+ - Fixtures
+
+args:
+ autoMockableImports: ["BitwardenKit", "BitwardenSdk", "Combine"]
+ autoMockableTestableImports: ["BitwardenShared"]
\ No newline at end of file
diff --git a/Mintfile b/Mintfile
index 3a9056208a..0436c70ebf 100644
--- a/Mintfile
+++ b/Mintfile
@@ -3,3 +3,4 @@ nicklockwood/SwiftFormat@0.56.4
SwiftGen/SwiftGen@6.6.3
realm/SwiftLint@0.59.1
yonaskolb/xcodegen@2.43.0
+krzysztofzablocki/Sourcery@2.2.7
diff --git a/Sourcery/Templates/AutoMockable.stencil b/Sourcery/Templates/AutoMockable.stencil
new file mode 100644
index 0000000000..649205ea99
--- /dev/null
+++ b/Sourcery/Templates/AutoMockable.stencil
@@ -0,0 +1,136 @@
+// swiftlint:disable line_length
+// swiftlint:disable variable_name
+
+import Foundation
+
+{% for import in argument.autoMockableImports %}
+import {{ import }}
+{% endfor %}
+
+{% for import in argument.autoMockableTestableImports %}
+@testable import {{ import }}
+{% endfor %}
+
+{% macro swiftifyMethodName name %}{{ name | replace:"(","_" | replace:")","" | replace:":","_" | replace:"`","" | snakeToCamelCase | lowerFirstWord }}{% endmacro %}
+
+{% macro methodThrowableErrorDeclaration method %}
+ var {% call swiftifyMethodName method.selectorName %}Error: Error?
+{% endmacro %}
+
+{% macro methodThrowableErrorUsage method %}
+ if let error = {% call swiftifyMethodName method.selectorName %}Error {
+ throw error
+ }
+{% endmacro %}
+
+{% macro methodReceivedParameters method %}
+ {%if method.parameters.count == 1 %}
+ {% set receivedVarName %}{% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}{% endfor %}{% endset %}
+ {{ receivedVarName }}{% for param in method.parameters %} = {{ param.name }}{% endfor %}
+ {% call swiftifyMethodName method.selectorName %}ReceivedInvocations.append({{ receivedVarName }}!)
+ {% else %}
+ {% if not method.parameters.count == 0 %}
+ {% set receivedVarName %}{% call swiftifyMethodName method.selectorName %}ReceivedArguments{% endset %}
+ {{ receivedVarName }} = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}){% if method.isGeneric %} as? ({% for param in method.parameters %}{{ param.name }}: {% call parameterType param %}{% if not forloop.last%}, {% endif %}{% endfor %}){% endif %}
+ {% call swiftifyMethodName method.selectorName %}ReceivedInvocations.append({{ receivedVarName }}!)
+ {% endif %}
+ {% endif %}
+{% endmacro %}
+
+{% macro methodReturnType method %}{% if method.returnTypeName.isVoid %}Void{% elif method.annotations["GenericReturn"] %}{{ method.annotations["GenericReturn"] }}{% else %}{{ method.returnTypeName }}{% endif %}{% endmacro %}
+
+{% macro parameterType param %}{% if param.annotations["Generic"] %}{{ param.annotations["Generic"] }}{% else %}{{ param.unwrappedTypeName if param.typeAttributes.escaping else param.typeName }}{% endif %}{% endmacro %}
+{% macro parameterArguments param %}{% if param.typeAttributes.escaping %}@escaping {% endif %}{% endmacro %}
+
+{% macro callClosure method %}return {{ 'try ' if method.throws }}{% if method.isGeneric %}({% endif %}{% call methodClosureName method %}.map({ {{ 'try ' if method.throws }}$0({% call methodClosureCallParameters method %}) }) ?? {% call swiftifyMethodName method.selectorName %}ReturnValue{% if method.isGeneric %}) as! {{ method.returnTypeName }}{% endif %}{% endmacro %}
+{% macro methodClosureName method %}{% call swiftifyMethodName method.selectorName %}Closure{% endmacro %}
+
+{% macro methodClosureDeclaration method %}
+ var {% call methodClosureName method %}: (({% for param in method.parameters %}{% call parameterArguments param %}{% call parameterType param %}{% if not forloop.last %}, {% endif %}{% endfor %}) {% if method.throws %}throws {% endif %}-> {% if method.isInitializer %}Void{% else %}{% call methodReturnType method %}{% endif %})?
+{% endmacro %}
+
+{% macro methodClosureCallParameters method %}{% for param in method.parameters %}{{ param.name }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endmacro %}
+{% macro methodClosureCallParametersFromArguments method %}{% for param in method.parameters %}{% call receivedArgumentsVarName method %}!{% if method.parameters.count > 1 %}.{{ param.name }}{% if not forloop.last %}, {% endif %}{% endif %}{% endfor %}{% endmacro %}
+
+{% macro receivedArgumentsVarName method %}{% if method.parameters.count == 1 %}{% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}{% endfor %}{% else %}{% call swiftifyMethodName method.selectorName %}ReceivedArguments{% endif %}{% endmacro %}
+
+{% macro mockMethod method %}
+ //MARK: - {{ method.shortName }}
+
+ {% if method.throws %}
+ {% call methodThrowableErrorDeclaration method %}
+ {% endif %}
+ {% if not method.isInitializer %}
+ var {% call swiftifyMethodName method.selectorName %}CalledCount = 0
+ var {% call swiftifyMethodName method.selectorName %}Called: Bool {
+ return {% call swiftifyMethodName method.selectorName %}CalledCount > 0
+ }
+ {% endif %}
+ {% if method.parameters.count == 1 %}
+ var {% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {{ '(' if param.isClosure }}{{ param.typeName.unwrappedTypeName }}{{ ')' if param.isClosure }}?{% endfor %}
+ var {% call swiftifyMethodName method.selectorName %}ReceivedInvocations{% for param in method.parameters %}: [{{ '(' if param.isClosure }}{{ param.typeName.unwrappedTypeName }}{{ ')' if param.isClosure }}{%if param.typeName.isOptional%}?{%endif%}]{% endfor %} = []
+ {% elif not method.parameters.count == 0 %}
+ var {% call swiftifyMethodName method.selectorName %}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {% call parameterType param %}{{ ', ' if not forloop.last }}{% endfor %})?
+ var {% call swiftifyMethodName method.selectorName %}ReceivedInvocations: [({% for param in method.parameters %}{{ param.name }}: {% call parameterType param %}{{ ', ' if not forloop.last }}{% endfor %})] = []
+ {% endif %}
+ {% if not method.returnTypeName.isVoid and not method.isInitializer %}
+ var {% call swiftifyMethodName method.selectorName %}ReturnValue: {% call methodReturnType method %}{{ '!' if not method.isOptionalReturnType }}
+ {% endif %}
+ {% call methodClosureDeclaration method %}
+
+{% if method.isInitializer %}
+ required {{ method.name }} {
+ {% call methodReceivedParameters method %}
+ {% call methodClosureName method %}?({% call methodClosureCallParametersFromArguments method %})
+ }
+{% else %}
+ func {{ method.name }}{{ ' throws' if method.throws }}{% if not method.returnTypeName.isVoid %} -> {{ method.returnTypeName }}{% endif %} {
+ {% if method.throws %}
+ {% call methodThrowableErrorUsage method %}
+ {% endif %}
+ {% call swiftifyMethodName method.selectorName %}CalledCount += 1
+ {% call methodReceivedParameters method %}
+ {% if method.returnTypeName.isVoid %}
+ {% if method.throws %}try {% endif %}{% call methodClosureName method %}?({% call methodClosureCallParametersFromArguments method %})
+ {% else %}
+ {% call callClosure method %}
+ {% endif %}
+ }
+
+{% endif %}
+{% endmacro %}
+
+{% macro mockOptionalVariable variable %}
+ var {% call mockedVariableName variable %}: {{ variable.typeName }}
+{% endmacro %}
+
+{% macro mockNonOptionalArrayOrDictionaryVariable variable %}
+ var {% call mockedVariableName variable %}: {{ variable.typeName }} = {% if variable.isArray %}[]{% elif variable.isDictionary %}[:]{% endif %}
+{% endmacro %}
+
+{% macro mockNonOptionalVariable variable %}
+ var {% call mockedVariableName variable %}: {{ variable.typeName }} {
+ get {
+ return {% call underlyingMockedVariableName variable %}
+ }
+ set(value) {
+ {% call underlyingMockedVariableName variable %} = value
+ }
+ }
+ var {% call underlyingMockedVariableName variable %}: {{ variable.typeName }}!
+{% endmacro %}
+
+{% macro underlyingMockedVariableName variable %}underlying{{ variable.name|upperFirstLetter }}{% endmacro %}
+{% macro mockedVariableName variable %}{{ variable.name }}{% endmacro %}
+
+{% for type in types.protocols where type.based.AutoMockable or type|annotated:"AutoMockable" %}{% if type.name != "AutoMockable" %}
+class Mock{{ type.name }}: {{ type.name }} {
+{% for variable in type.allVariables|!definedInExtension %}
+ {% if variable.isOptional %}{% call mockOptionalVariable variable %}{% elif variable.isArray or variable.isDictionary %}{% call mockNonOptionalArrayOrDictionaryVariable variable %}{% else %}{% call mockNonOptionalVariable variable %}{% endif %}
+{% endfor %}
+
+{% for method in type.allMethods|!definedInExtension %}
+ {% call mockMethod method %}
+{% endfor %}
+}
+{% endif %}{% endfor %}
\ No newline at end of file
diff --git a/project-bwa.yml b/project-bwa.yml
index 9c5c3fe6db..5499936f1c 100644
--- a/project-bwa.yml
+++ b/project-bwa.yml
@@ -174,6 +174,8 @@ targets:
- "**/TestHelpers/*"
- "**/Fixtures/*"
- "**/__Snapshots__/*"
+ - "**/Sourcery/Generated/*"
+ - "**/sourcery.yml"
- path: AuthenticatorShared
includes:
- "**/__Snapshots__/*"
@@ -184,6 +186,8 @@ targets:
optional: true
- path: AuthenticatorShared/Core/Vault/Services/Importers/Support/Generated/GoogleAuth.pb.swift
optional: true
+ - path: AuthenticatorShared/Sourcery/sourcery.yml
+ buildPhase: none
dependencies:
- target: BitwardenKit/AuthenticatorBridgeKit
- package: BitwardenSdk
@@ -210,6 +214,15 @@ targets:
$SRCROOT/AuthenticatorShared/Core/Vault/Services/Importers/Support/GoogleAuth.proto
outputFiles:
- $(SRCROOT)/AuthenticatorShared/Core/Vault/Services/Importers/Support/Generated/GoogleAuth.pb.swift
+ - name: Sourcery
+ script: |
+ if [[ ! "$PATH" =~ "/opt/homebrew/bin" ]]; then
+ PATH="/opt/homebrew/bin:$PATH"
+ fi
+ mint run sourcery --config AuthenticatorShared/Sourcery/sourcery.yml
+ basedOnDependencyAnalysis: false
+ outputFiles:
+ - $(SRCROOT)/AuthenticatorShared/Sourcery/Generated/AutoMockable.generated.swift
AuthenticatorSharedTests:
type: bundle.unit-test
platform: iOS
@@ -225,6 +238,10 @@ targets:
- "**/TestHelpers/*"
- "**/Fixtures/*"
- path: GlobalTestHelpers-bwa
+ - path: AuthenticatorShared/Sourcery/Generated
+ optional: true
+ - path: AuthenticatorShared/Sourcery/Generated/AutoMockable.generated.swift
+ optional: true
dependencies:
- target: Authenticator
- target: AuthenticatorShared
diff --git a/project-pm.yml b/project-pm.yml
index 679bb92690..7bcee9cb5b 100644
--- a/project-pm.yml
+++ b/project-pm.yml
@@ -362,6 +362,8 @@ targets:
- "**/TestHelpers/*"
- "**/Fixtures/*"
- "**/__Snapshots__/*"
+ - "**/Sourcery/Generated/*"
+ - "**/sourcery.yml"
- path: BitwardenShared
includes:
- "**/__Snapshots__/*"
@@ -373,6 +375,8 @@ targets:
- path: BitwardenShared/UI/Platform/Application/Support/Generated/Localizations.swift
optional: true
- path: BitwardenWatchShared
+ - path: BitwardenShared/Sourcery/sourcery.yml
+ buildPhase: none
dependencies:
- package: BitwardenSdk
- package: SwiftUIIntrospect
@@ -391,6 +395,15 @@ targets:
- $(SRCROOT)/BitwardenShared/UI/Platform/Application/Support/Generated/Assets.swift
- $(SRCROOT)/BitwardenShared/UI/Platform/Application/Support/Generated/Fonts.swift
- $(SRCROOT)/BitwardenShared/UI/Platform/Application/Support/Generated/Localizations.swift
+ - name: Sourcery
+ script: |
+ if [[ ! "$PATH" =~ "/opt/homebrew/bin" ]]; then
+ PATH="/opt/homebrew/bin:$PATH"
+ fi
+ mint run sourcery --config BitwardenShared/Sourcery/sourcery.yml
+ basedOnDependencyAnalysis: false
+ outputFiles:
+ - $(SRCROOT)/BitwardenShared/Sourcery/Generated/AutoMockable.generated.swift
BitwardenSharedTests:
type: bundle.unit-test
platform: iOS
@@ -409,6 +422,10 @@ targets:
- "**/TestHelpers/*"
- "**/Fixtures/*"
- path: GlobalTestHelpers
+ - path: BitwardenShared/Sourcery/Generated
+ optional: true
+ - path: BitwardenShared/Sourcery/Generated/AutoMockable.generated.swift
+ optional: true
dependencies:
- target: Bitwarden
- target: BitwardenShared