Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Foundation

@testable import Data

final class UserDefaultsManagerMock: UserDefaultsManagerProtocol, @unchecked Sendable {

private var storage: [String: Any] = [:]
private var synchronizeCallCount = 0

var didCallSynchronize: Bool { synchronizeCallCount > 0 }
var synchronizeCallCountValue: Int { synchronizeCallCount }

func set(_ value: Any?, for key: String) {
if let value = value {
storage[key] = value
} else {
storage.removeValue(forKey: key)
}
}

func setObject<T: Codable>(_ value: T?, key: String) {
if let value = value {
storage[key] = value
} else {
storage.removeValue(forKey: key)
}
}

func getStringValue(for key: String) -> String? {
return storage[key] as? String
}

func getBooleanValue(for key: String) -> Bool {
return storage[key] as? Bool ?? false
}

func getIntValue(for key: String) -> Int {
return storage[key] as? Int ?? 0
}

func getArray(for key: String) -> [Any]? {
return storage[key] as? [Any]
}

func getDataValue(for key: String) -> Data? {
return storage[key] as? Data
}

func getObject<T: Codable>(ofType: T.Type, key: String) -> T? {
return storage[key] as? T
}

func getValue(for key: String) -> Any? {
return storage[key]
}

func getAllKeys(withPrefix prefix: String) -> [String] {
return storage.keys.filter { $0.hasPrefix(prefix) }
}

func clearData(forKeys keys: [String]) {
keys.forEach { storage.removeValue(forKey: $0) }
}

func clearDataForCommonKeys() {
storage.removeAll()
}
Comment thread
thinh2k1310 marked this conversation as resolved.

func synchronize() {
synchronizeCallCount += 1
}

// Test helper methods
func reset() {
storage.removeAll()
synchronizeCallCount = 0
}

func setStorageValue(_ value: Any?, forKey key: String) {
storage[key] = value
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import Foundation
import Testing

@testable import Data
import Domain

@Suite("RatingPromptStorage")
struct RatingPromptStorageTests {

@Test("getRatingPromptData returns data with default values when no data is stored")
func getRatingPromptDataReturnsDataWithDefaultValuesWhenNoDataIsStored() {
let userDefaultsManager = UserDefaultsManagerMock()
let storage = RatingPromptStorage(userDefaultsManager: userDefaultsManager)

let data = storage.getRatingPromptData()

#expect(data.appLaunchCount == 0)
#expect(data.firstLaunchDate == nil)
#expect(data.lastPromptedVersion == nil)
#expect(data.significantEventCount == 0)
}

@Test("getRatingPromptData returns stored values")
func getRatingPromptDataReturnsStoredValues() throws {
let userDefaultsManager = UserDefaultsManagerMock()
let testDate = Date()
let encodedDate = try JSONEncoder().encode(testDate)

userDefaultsManager.setStorageValue(5, forKey: UserDefaultsKey.ratingPromptAppLaunchCount.rawValue)
userDefaultsManager.setStorageValue(encodedDate, forKey: UserDefaultsKey.ratingPromptFirstLaunchDate.rawValue)
userDefaultsManager.setStorageValue("1.2.0", forKey: UserDefaultsKey.ratingPromptLastPromptedVersion.rawValue)
userDefaultsManager.setStorageValue(3, forKey: UserDefaultsKey.ratingPromptSignificantEventCount.rawValue)

let storage = RatingPromptStorage(userDefaultsManager: userDefaultsManager)
let data = storage.getRatingPromptData()

#expect(data.appLaunchCount == 5)
#expect(data.firstLaunchDate?.timeIntervalSince1970 == testDate.timeIntervalSince1970)
#expect(data.lastPromptedVersion == "1.2.0")
#expect(data.significantEventCount == 3)
}

@Test("getRatingPromptData handles invalid date data gracefully")
func getRatingPromptDataHandlesInvalidDateDataGracefully() {
let userDefaultsManager = UserDefaultsManagerMock()
let invalidDateData = Data([0x00, 0x01, 0x02]) // Invalid JSON for Date

userDefaultsManager.setStorageValue(invalidDateData, forKey: UserDefaultsKey.ratingPromptFirstLaunchDate.rawValue)

let storage = RatingPromptStorage(userDefaultsManager: userDefaultsManager)
let data = storage.getRatingPromptData()

#expect(data.firstLaunchDate == nil)
}

@Test("recordAppLaunch increments launch count")
func recordAppLaunchIncrementsLaunchCount() {
let userDefaultsManager = UserDefaultsManagerMock()
userDefaultsManager.setStorageValue(5, forKey: UserDefaultsKey.ratingPromptAppLaunchCount.rawValue)

let storage = RatingPromptStorage(userDefaultsManager: userDefaultsManager)
storage.recordAppLaunch()

let updatedCount = userDefaultsManager.getIntValue(for: UserDefaultsKey.ratingPromptAppLaunchCount.rawValue)
#expect(updatedCount == 6)
#expect(userDefaultsManager.didCallSynchronize)
}

@Test("recordAppLaunch sets first launch date when not already set")
func recordAppLaunchSetsFirstLaunchDateWhenNotAlreadySet() throws {
let userDefaultsManager = UserDefaultsManagerMock()
let storage = RatingPromptStorage(userDefaultsManager: userDefaultsManager)

storage.recordAppLaunch()

let dateData = userDefaultsManager.getDataValue(for: UserDefaultsKey.ratingPromptFirstLaunchDate.rawValue)
#expect(dateData != nil)

let decodedDate = try JSONDecoder().decode(Date.self, from: dateData!)
let now = Date()
// Allow for small time difference (within 1 second)
#expect(abs(decodedDate.timeIntervalSince(now)) < 1.0)
}

@Test("recordAppLaunch does not overwrite existing first launch date")
func recordAppLaunchDoesNotOverwriteExistingFirstLaunchDate() throws {
let userDefaultsManager = UserDefaultsManagerMock()
let existingDate = Date().addingTimeInterval(-1000)
let encodedExistingDate = try JSONEncoder().encode(existingDate)

userDefaultsManager.setStorageValue(encodedExistingDate, forKey: UserDefaultsKey.ratingPromptFirstLaunchDate.rawValue)

let storage = RatingPromptStorage(userDefaultsManager: userDefaultsManager)
storage.recordAppLaunch()

let dateData = userDefaultsManager.getDataValue(for: UserDefaultsKey.ratingPromptFirstLaunchDate.rawValue)!
let storedDate = try JSONDecoder().decode(Date.self, from: dateData)

#expect(storedDate.timeIntervalSince1970 == existingDate.timeIntervalSince1970)
}

@Test("recordSignificantEvent increments significant event count")
func recordSignificantEventIncrementsSignificantEventCount() {
let userDefaultsManager = UserDefaultsManagerMock()
userDefaultsManager.setStorageValue(3, forKey: UserDefaultsKey.ratingPromptSignificantEventCount.rawValue)

let storage = RatingPromptStorage(userDefaultsManager: userDefaultsManager)
storage.recordSignificantEvent()

let updatedCount = userDefaultsManager.getIntValue(for: UserDefaultsKey.ratingPromptSignificantEventCount.rawValue)
#expect(updatedCount == 4)
#expect(userDefaultsManager.didCallSynchronize)
}

@Test("recordPromptShown stores the app version")
func recordPromptShownStoresTheAppVersion() {
let userDefaultsManager = UserDefaultsManagerMock()
let storage = RatingPromptStorage(userDefaultsManager: userDefaultsManager)

storage.recordPromptShown(for: "2.1.0")

let storedVersion = userDefaultsManager.getStringValue(for: UserDefaultsKey.ratingPromptLastPromptedVersion.rawValue)
#expect(storedVersion == "2.1.0")
#expect(userDefaultsManager.didCallSynchronize)
}

@Test("resetCounters resets launch and event counters to zero")
func resetCountersResetsLaunchAndEventCountersToZero() {
let userDefaultsManager = UserDefaultsManagerMock()
userDefaultsManager.setStorageValue(10, forKey: UserDefaultsKey.ratingPromptAppLaunchCount.rawValue)
userDefaultsManager.setStorageValue(5, forKey: UserDefaultsKey.ratingPromptSignificantEventCount.rawValue)

let storage = RatingPromptStorage(userDefaultsManager: userDefaultsManager)
storage.resetCounters()

let launchCount = userDefaultsManager.getIntValue(for: UserDefaultsKey.ratingPromptAppLaunchCount.rawValue)
let eventCount = userDefaultsManager.getIntValue(for: UserDefaultsKey.ratingPromptSignificantEventCount.rawValue)

#expect(launchCount == 0)
#expect(eventCount == 0)
#expect(userDefaultsManager.didCallSynchronize)
}

@Test("clearAllData removes all rating prompt related data")
func clearAllDataRemovesAllRatingPromptRelatedData() throws {
let userDefaultsManager = UserDefaultsManagerMock()
let testDate = Date()
let encodedDate = try JSONEncoder().encode(testDate)

// Set up initial data
userDefaultsManager.setStorageValue(10, forKey: UserDefaultsKey.ratingPromptAppLaunchCount.rawValue)
userDefaultsManager.setStorageValue(encodedDate, forKey: UserDefaultsKey.ratingPromptFirstLaunchDate.rawValue)
userDefaultsManager.setStorageValue("1.0.0", forKey: UserDefaultsKey.ratingPromptLastPromptedVersion.rawValue)
userDefaultsManager.setStorageValue(5, forKey: UserDefaultsKey.ratingPromptSignificantEventCount.rawValue)

let storage = RatingPromptStorage(userDefaultsManager: userDefaultsManager)
storage.clearAllData()

// Verify all data is cleared
let data = storage.getRatingPromptData()
#expect(data.appLaunchCount == 0)
#expect(data.firstLaunchDate == nil)
#expect(data.lastPromptedVersion == nil)
#expect(data.significantEventCount == 0)
#expect(userDefaultsManager.didCallSynchronize)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Foundation
import Testing

@testable import Data
import Domain

@Suite("DefaultRatingPromptPresenter")
struct DefaultRatingPromptPresenterTests {

@Test("calls requestReview on StoreReviewController when show is called and returns result")
func callsRequestReviewOnStoreReviewControllerWhenShowIsCalledAndReturnsResult() async {
let storeReviewController = SpyStoreReviewController()
let presenter = DefaultRatingPromptPresenter(storeReviewController: storeReviewController)

let result = await presenter.show()

let callCount = await storeReviewController.requestReviewCallCount
#expect(callCount == 1)
#expect(result == true)
}

@Test("returns false when store review controller returns false")
func returnsFalseWhenStoreReviewControllerReturnsFalse() async {
let storeReviewController = SpyStoreReviewController(shouldReturnTrue: false)
let presenter = DefaultRatingPromptPresenter(storeReviewController: storeReviewController)

let result = await presenter.show()

#expect(result == false)
}

@Test("multiple calls to show result in multiple requestReview calls")
func multipleCallsToShowResultInMultipleRequestReviewCalls() async {
let storeReviewController = SpyStoreReviewController()
let presenter = DefaultRatingPromptPresenter(storeReviewController: storeReviewController)

let result1 = await presenter.show()
let result2 = await presenter.show()
let result3 = await presenter.show()

let callCount = await storeReviewController.requestReviewCallCount
#expect(callCount == 3)
#expect(result1 == true)
#expect(result2 == true)
#expect(result3 == true)
}
}

// MARK: - Test Double

@MainActor
private final class SpyStoreReviewController: StoreReviewControllerProtocol, @unchecked Sendable {

private(set) var requestReviewCallCount = 0
private let shouldReturnTrue: Bool

init(shouldReturnTrue: Bool = true) {
self.shouldReturnTrue = shouldReturnTrue
}

func requestReview() async -> Bool {
requestReviewCallCount += 1
return shouldReturnTrue
}
Comment thread
thinh2k1310 marked this conversation as resolved.
}
Loading