diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/build_and_test.yml similarity index 68% rename from .github/workflows/unit_tests.yml rename to .github/workflows/build_and_test.yml index cb9d2a9..f80737b 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/build_and_test.yml @@ -1,7 +1,7 @@ # This workflow will test a Swift project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift -name: Unit Tests +name: Xcode on: - pull_request @@ -12,7 +12,7 @@ concurrency: jobs: test-ios: - name: iOS 18.1 + name: Build and Test (iOS 18.1) runs-on: macos-latest env: DEVELOPER_DIR: "/Applications/Xcode_16.1.app/Contents/Developer" @@ -22,12 +22,20 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - # Run Unit Tests + # Build + - name: Build + run: | + xcodebuild build\ + -scheme EDXMobileAnalytics \ + -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation)' \ + -skipPackagePluginValidation \ + -skipMacroValidation + + # Run Unit Tests - name: Run tests run: | xcodebuild test \ -scheme EDXMobileAnalytics \ - -sdk iphonesimulator \ -destination "OS=18.1,name=iPhone 16 Pro" \ -skipPackagePluginValidation \ -skipMacroValidation diff --git a/.github/workflows/xcodebuild.yml b/.github/workflows/xcodebuild.yml deleted file mode 100644 index 96b8419..0000000 --- a/.github/workflows/xcodebuild.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: XCodeBuild - -on: - workflow_dispatch: - - pull_request: - -jobs: - build: - name: Build - runs-on: macos-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup - run: - xcodes select 16.1 - - - name: Build - run: - xcodebuild -scheme EDXMobileAnalytics -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation)' -skipPackagePluginValidation -skipMacroValidation build diff --git a/Package.resolved b/Package.resolved index d269ff0..75b1b03 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "24865ce1a5bdb12bdf2082dc9a6b3b327986ee32238127921bf6e08f58b56e7f", + "originHash" : "aa75406ff97bf77af672c326455e75425b255f9dca089e8766327c96248e2f30", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -217,6 +217,15 @@ "version" : "1.28.2" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, { "identity" : "swiftlintplugins", "kind" : "remoteSourceControl", @@ -243,6 +252,15 @@ "revision" : "be9dbcc7b86811bc131539a20c6f9c2d3e56919f", "version" : "2.9.1" } + }, + { + "identity" : "testablemacro", + "kind" : "remoteSourceControl", + "location" : "https://github.com/fernandolucheti/TestableMacro.git", + "state" : { + "revision" : "84b111ae97f2e5cb4d84115edd0ebc145147ee00", + "version" : "0.0.2" + } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index 3076f37..ea0225a 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,8 @@ let package = Package( .package(url: "https://github.com/segmentio/analytics-swift.git", from: "1.5.3"), .package(url: "https://github.com/segment-integrations/analytics-swift-firebase", from: "1.3.5"), .package(url: "https://github.com/braze-inc/braze-segment-swift.git", from: "2.2.0"), - .package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "0.57.0") + .package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "0.57.0"), + .package(url: "https://github.com/fernandolucheti/TestableMacro.git", from: "0.0.2") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -30,7 +31,8 @@ let package = Package( .product(name: "OEXFoundation", package: "openedx-app-foundation-ios"), .product(name: "Segment", package: "analytics-swift"), .product(name: "SegmentFirebase", package: "analytics-swift-firebase"), - .product(name: "SegmentBraze", package: "braze-segment-swift") + .product(name: "SegmentBraze", package: "braze-segment-swift"), + .product(name: "TestableMacro", package: "TestableMacro") ], plugins: [ .plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins") diff --git a/README.md b/README.md index 4f28260..b5deeef 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # EDXMobileAnalytics -[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](LICENSE) EDXMobileAnalytics is a Swift plugin for integrating analytics in edX iOS mobile applications. This plugin includes support for **Segment Analytics** and **Braze** (via Segment) to help developers efficiently track user behavior and events within their apps. @@ -57,10 +57,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } private func initPlugins() { - // - Segment analytic + // - Segment analyticы let SegmentAnalyticsService = SegmentAnalyticsService( writeKey: "your_writeKey", - firebaseAnalyticSourceIsSegment: true // or false + addFirebaseAnalytics: true // or false ) pluginManager.addPlugin(analyticsService: SegmentAnalyticsService) @@ -69,12 +69,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { pluginManager.addPlugin( pushNotificationsProvider: BrazeProvider( - segmentAnalyticService: SegmentAnalyticsService + segmentAnalyticsService: SegmentAnalyticsService ), pushNotificationsListener: BrazeListener( deepLinkManager: deepLinkManager, - segmentAnalyticService: SegmentAnalyticsService + segmentAnalyticsService: SegmentAnalyticsService ) ) @@ -103,7 +103,7 @@ func logScreenEvent(_ event: String, parameters: [String: Any]?) ## License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. +This project is licensed under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for details. ## Contact diff --git a/Sources/EDXMobileAnalytics/Braze/BrazeListener.swift b/Sources/EDXMobileAnalytics/Braze/BrazeListener.swift index dd325b7..90e2e60 100644 --- a/Sources/EDXMobileAnalytics/Braze/BrazeListener.swift +++ b/Sources/EDXMobileAnalytics/Braze/BrazeListener.swift @@ -7,18 +7,20 @@ import Foundation import OEXFoundation +import TestableMacro +@Testable public class BrazeListener: PushNotificationsListener { private let deepLinkManager: DeepLinkManagerProtocol - private let segmentAnalyticService: SegmentAnalyticsServiceProtocol? + private let segmentAnalyticsService: SegmentAnalyticsServiceProtocol? public init( deepLinkManager: DeepLinkManagerProtocol, - segmentAnalyticService: SegmentAnalyticsServiceProtocol? = nil + segmentAnalyticsService: SegmentAnalyticsServiceProtocol? = nil ) { self.deepLinkManager = deepLinkManager - self.segmentAnalyticService = segmentAnalyticService + self.segmentAnalyticsService = segmentAnalyticsService } public func shouldListenNotification(userinfo: [AnyHashable: Any]) -> Bool { @@ -30,7 +32,7 @@ public class BrazeListener: PushNotificationsListener { public func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) { guard shouldListenNotification(userinfo: userInfo) else { return } - segmentAnalyticService?.receivedRemoteNotification(userInfo: userInfo) + segmentAnalyticsService?.receivedRemoteNotification(userInfo: userInfo) deepLinkManager.processLinkFrom(userInfo: userInfo) } } diff --git a/Sources/EDXMobileAnalytics/Braze/BrazeProvider.swift b/Sources/EDXMobileAnalytics/Braze/BrazeProvider.swift index c0c44ec..3f290f7 100644 --- a/Sources/EDXMobileAnalytics/Braze/BrazeProvider.swift +++ b/Sources/EDXMobileAnalytics/Braze/BrazeProvider.swift @@ -11,15 +11,14 @@ import SegmentBraze final public class BrazeProvider: PushNotificationsProvider { - private let segmentAnalyticService: SegmentAnalyticsServiceProtocol? + private let segmentAnalyticsService: SegmentAnalyticsServiceProtocol - public init(segmentAnalyticService: SegmentAnalyticsServiceProtocol? = nil) { - self.segmentAnalyticService = segmentAnalyticService + public init(segmentAnalyticsService: SegmentAnalyticsServiceProtocol) { + self.segmentAnalyticsService = segmentAnalyticsService } public func didRegisterWithDeviceToken(deviceToken: Data) { - guard let segmentService = segmentAnalyticService else { return } - segmentService.add( + segmentAnalyticsService.add( plugin: BrazeDestination( additionalConfiguration: { configuration in configuration.logger.level = .info @@ -29,6 +28,6 @@ final public class BrazeProvider: PushNotificationsProvider { ) ) - segmentService.registeredForRemoteNotifications(deviceToken: deviceToken) + segmentAnalyticsService.registeredForRemoteNotifications(deviceToken: deviceToken) } } diff --git a/Sources/EDXMobileAnalytics/EDXMobileAnalytics.swift b/Sources/EDXMobileAnalytics/EDXMobileAnalytics.swift deleted file mode 100644 index 08b22b8..0000000 --- a/Sources/EDXMobileAnalytics/EDXMobileAnalytics.swift +++ /dev/null @@ -1,2 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book diff --git a/Sources/EDXMobileAnalytics/Segment/SegmentAnalyticsService.swift b/Sources/EDXMobileAnalytics/Segment/SegmentAnalyticsService.swift index aed47df..51063e4 100644 --- a/Sources/EDXMobileAnalytics/Segment/SegmentAnalyticsService.swift +++ b/Sources/EDXMobileAnalytics/Segment/SegmentAnalyticsService.swift @@ -9,6 +9,7 @@ import Foundation import OEXFoundation @preconcurrency import Segment import SegmentFirebase +import TestableMacro public protocol SegmentAnalyticsServiceProtocol: AnalyticsService { func receivedRemoteNotification(userInfo: [AnyHashable: Any]) @@ -16,49 +17,53 @@ public protocol SegmentAnalyticsServiceProtocol: AnalyticsService { func add(plugin: Plugin) } +@Testable final public class SegmentAnalyticsService: SegmentAnalyticsServiceProtocol { - private let analytics: Analytics? + private let analytics: Analytics // Init manager - public init(writeKey: String, firebaseAnalyticSourceIsSegment: Bool) { + public init(writeKey: String, addFirebaseAnalytics: Bool) { let configuration = Configuration(writeKey: writeKey) .trackApplicationLifecycleEvents(true) .flushInterval(10) analytics = Analytics(configuration: configuration) - if firebaseAnalyticSourceIsSegment { - analytics?.add(plugin: FirebaseDestination()) + if addFirebaseAnalytics { + analytics.add(plugin: FirebaseDestination()) } } public func identify(id: String, username: String?, email: String?) { - guard let email = email, let username = username else { return } + guard let email = email, let username = username else { + assertionFailure("Email and Username are required for identifying user") + return + } let traits: [String: String] = [ "email": email, "username": username ] - analytics?.identify(userId: id, traits: traits) + analytics.identify(userId: id, traits: traits) } public func logEvent(_ event: String, parameters: [String: Any]?) { - analytics?.track( + analytics.track( name: event, properties: parameters ) } public func logScreenEvent(_ event: String, parameters: [String: Any]?) { - analytics?.screen(title: event, properties: parameters) + analytics.screen(title: event, properties: parameters) } public func receivedRemoteNotification(userInfo: [AnyHashable: Any]) { - analytics?.receivedRemoteNotification(userInfo: userInfo) + analytics.receivedRemoteNotification(userInfo: userInfo) } public func registeredForRemoteNotifications(deviceToken: Data) { - analytics?.registeredForRemoteNotifications(deviceToken: deviceToken) + analytics.registeredForRemoteNotifications(deviceToken: deviceToken) } public func add(plugin: Plugin) { - analytics?.add(plugin: plugin) + analytics.add(plugin: plugin) } } diff --git a/Tests/EDXMobileAnalyticsTests/BrazeListenerTests.swift b/Tests/EDXMobileAnalyticsTests/BrazeListenerTests.swift index 556b454..4241a59 100644 --- a/Tests/EDXMobileAnalyticsTests/BrazeListenerTests.swift +++ b/Tests/EDXMobileAnalyticsTests/BrazeListenerTests.swift @@ -15,11 +15,11 @@ private extension BrazeListenerTests { @MainActor @Suite(".shouldListenNotification") struct ShouldListenNotification { let deepLinkMananger = DeepLinkManagerProtocolMock() - let segmentAnalyticService = SegmentAnalyticsServiceMock() + let segmentAnalyticsService = SegmentAnalyticsServiceMock() var brazeListener: BrazeListener { BrazeListener( deepLinkManager: deepLinkMananger, - segmentAnalyticService: segmentAnalyticService + segmentAnalyticsService: segmentAnalyticsService ) } @@ -34,26 +34,26 @@ private extension BrazeListenerTests { @MainActor @Suite(".didReceiveRemoteNotification") struct DidReceiveRemoteNotification { let deepLinkMananger = DeepLinkManagerProtocolMock() - let segmentAnalyticService = SegmentAnalyticsServiceMock() + let segmentAnalyticsService = SegmentAnalyticsServiceMock() var brazeListener: BrazeListener { BrazeListener( deepLinkManager: deepLinkMananger, - segmentAnalyticService: segmentAnalyticService + segmentAnalyticsService: segmentAnalyticsService ) } @Test("When shouldListenNotification returns false should do nothing") func check1() async throws { brazeListener.didReceiveRemoteNotification(userInfo: TestData.dataWithoutNeededKey) - #expect(segmentAnalyticService.receivedRemoteNotificationCount == 0) + #expect(segmentAnalyticsService.receivedRemoteNotificationCount == 0) #expect(deepLinkMananger.processLinkFromCallsCount == 0) } @Test("When shouldListenNotification returns true should call processLinkFrom") func check2() async throws { brazeListener.didReceiveRemoteNotification(userInfo: TestData.dataWithNeededKey) - #expect(segmentAnalyticService.receivedRemoteNotificationCount == 1) + #expect(segmentAnalyticsService.receivedRemoteNotificationCount == 1) #expect(deepLinkMananger.processLinkFromCallsCount == 1) #expect( - segmentAnalyticService.receivedRemoteNotificationUserInfo["ab"] as? [String: String] == + segmentAnalyticsService.receivedRemoteNotificationUserInfo["ab"] as? [String: String] == TestData.dataWithNeededKey["ab"] as? [String: String] ) } @@ -64,9 +64,8 @@ private extension BrazeListenerTests { @Test("Should set default value") func check1() async throws { let deepLinkMananger = DeepLinkManagerProtocolMock() let brazeListener = BrazeListener(deepLinkManager: deepLinkMananger) - let analyticService = Mirror(reflecting: brazeListener) - .descendant("segmentAnalyticService") as? SegmentAnalyticsServiceProtocol - #expect(analyticService == nil) + let analyticsService = brazeListener.testHooks.segmentAnalyticsService + #expect(analyticsService == nil) } } } diff --git a/Tests/EDXMobileAnalyticsTests/BrazeProviderTests.swift b/Tests/EDXMobileAnalyticsTests/BrazeProviderTests.swift index 0998ca6..72a7ee9 100644 --- a/Tests/EDXMobileAnalyticsTests/BrazeProviderTests.swift +++ b/Tests/EDXMobileAnalyticsTests/BrazeProviderTests.swift @@ -9,35 +9,16 @@ private extension BrazeProviderTests { } @Suite struct BrazeProviderTests { - @Suite(".init") struct InitTest { - @Test("Should set default value") func check1() async throws { - let brazeProvider = BrazeProvider() - let analyticService = Mirror(reflecting: brazeProvider) - .descendant("segmentAnalyticService") as? SegmentAnalyticsServiceProtocol - #expect(analyticService == nil) - } - } - @Suite(".didRegisterWithDeviceToken") struct DidRegisterWithDeviceTokenTest { - let segmentAnalyticService = SegmentAnalyticsServiceMock() - - @Test("When segmentAnalyticService is nil should do nothing") func check1() async throws { - let brazeProviderWithNilService: BrazeProvider = .init() - brazeProviderWithNilService.didRegisterWithDeviceToken(deviceToken: TestData.deviceToken) - #expect(segmentAnalyticService.addPluginCallsCount == 0) - #expect(segmentAnalyticService.addPluginCalledWith.isEmpty) - #expect(segmentAnalyticService.registeredForRemoteNotificationsCallsCount == 0) - #expect(segmentAnalyticService.registeredForRemoteNotificationsDeviceToken.isEmpty) - } - - @Test("When segmentAnalyticService is not nil should call segmentAnalyticService methods") + let segmentAnalyticsService = SegmentAnalyticsServiceMock() + @Test("Should call segmentAnalyticsService methods") func check2() async throws { - let brazeProvider: BrazeProvider = .init(segmentAnalyticService: segmentAnalyticService) + let brazeProvider: BrazeProvider = .init(segmentAnalyticsService: segmentAnalyticsService) brazeProvider.didRegisterWithDeviceToken(deviceToken: TestData.deviceToken) - #expect(segmentAnalyticService.addPluginCallsCount == 1) - #expect(segmentAnalyticService.addPluginCalledWith.count == 1) - #expect(segmentAnalyticService.registeredForRemoteNotificationsCallsCount == 1) - #expect(segmentAnalyticService.registeredForRemoteNotificationsDeviceToken == TestData.deviceToken) + #expect(segmentAnalyticsService.addPluginCallsCount == 1) + #expect(segmentAnalyticsService.addPluginCalledWith.count == 1) + #expect(segmentAnalyticsService.registeredForRemoteNotificationsCallsCount == 1) + #expect(segmentAnalyticsService.registeredForRemoteNotificationsDeviceToken == TestData.deviceToken) } } } diff --git a/Tests/EDXMobileAnalyticsTests/SegmentAnalyticsServiceTests.swift b/Tests/EDXMobileAnalyticsTests/SegmentAnalyticsServiceTests.swift index 32e2d8f..fe96821 100644 --- a/Tests/EDXMobileAnalyticsTests/SegmentAnalyticsServiceTests.swift +++ b/Tests/EDXMobileAnalyticsTests/SegmentAnalyticsServiceTests.swift @@ -6,24 +6,26 @@ import Foundation @Suite struct SegmentAnalyticsServiceTests { @Suite(".init") struct InitTest { - @Test("When firebaseAnalyticSourceIsSegment is false shouldn't add plugin") func check1() async throws { - let analyticService = SegmentAnalyticsService( - writeKey: UUID().uuidString, - firebaseAnalyticSourceIsSegment: false + @Test("When addFirebaseAnalytics is false shouldn't add plugin") func check1() async throws { + let writeKey = UUID().uuidString + let analyticsService = SegmentAnalyticsService( + writeKey: writeKey, + addFirebaseAnalytics: false ) - let analytics = Mirror(reflecting: analyticService).descendant("analytics") as? Analytics - #expect(analytics != nil) - #expect(analytics?.find(pluginType: FirebaseDestination.self) == nil) + let analytics = analyticsService.testHooks.analytics + #expect(analytics.writeKey == writeKey) + #expect(analytics.find(pluginType: FirebaseDestination.self) == nil) } - @Test("When firebaseAnalyticSourceIsSegment is true should add plugin") func check2() async throws { - let analyticService = SegmentAnalyticsService( - writeKey: UUID().uuidString, - firebaseAnalyticSourceIsSegment: true + @Test("When addFirebaseAnalytics is true should add plugin") func check2() async throws { + let writeKey = UUID().uuidString + let analyticsService = SegmentAnalyticsService( + writeKey: writeKey, + addFirebaseAnalytics: true ) - let analytics = Mirror(reflecting: analyticService).descendant("analytics") as? Analytics - #expect(analytics != nil) - #expect(analytics?.find(pluginType: FirebaseDestination.self) != nil) + let analytics = analyticsService.testHooks.analytics + #expect(analytics.writeKey == writeKey) + #expect(analytics.find(pluginType: FirebaseDestination.self) != nil) } } }