diff --git a/GravatarApp.xcodeproj/project.pbxproj b/GravatarApp.xcodeproj/project.pbxproj index 1ddd88f8..12e6372a 100644 --- a/GravatarApp.xcodeproj/project.pbxproj +++ b/GravatarApp.xcodeproj/project.pbxproj @@ -172,6 +172,22 @@ knownRegions = ( en, Base, + de, + he, + ar, + "zh-Hans", + ja, + es, + it, + sv, + ko, + "zh-Hant", + tr, + "pt-BR", + ru, + fr, + id, + nl, ); mainGroup = 1EDD7F872DF9B94300E5F1B6; minimizedProjectReferenceProxies = 1; diff --git a/GravatarApp/Share/ShareViewModel.swift b/GravatarApp/Share/ShareViewModel.swift index 5f0f8656..d61e3815 100644 --- a/GravatarApp/Share/ShareViewModel.swift +++ b/GravatarApp/Share/ShareViewModel.swift @@ -21,7 +21,7 @@ class ShareViewModel: ObservableObject { private let networkMonitor: NetworkMonitor private let userDefaults: UserDefaults - let qrGenerator: QRGenerator + private let qrGenerator: QRGenerator @Published var storedUserEmail: String { didSet { userDefaults.set(storedUserEmail, forKey: StorageKeys.email) } @@ -57,7 +57,7 @@ class ShareViewModel: ObservableObject { } } - func setupObservers() { + private func setupObservers() { userSession.$profile .receive(on: RunLoop.main) .sink { [weak self] newProfile in diff --git a/GravatarAppTests/Helpers/ImageHelper.swift b/GravatarAppTests/Helpers/ImageHelper.swift index 5b795b58..25a4a801 100644 --- a/GravatarAppTests/Helpers/ImageHelper.swift +++ b/GravatarAppTests/Helpers/ImageHelper.swift @@ -22,7 +22,7 @@ enum ImageHelper { } static func dataFromImage(named: String, type: String) -> Data? { - guard let url = Bundle.main.url(forResource: named, withExtension: type) else { + guard let url = Bundle.testsBundle.url(forResource: named, withExtension: type) else { return nil } var data: Data? = nil diff --git a/GravatarAppTests/Helpers/URLSessionMock.swift b/GravatarAppTests/Helpers/URLSessionMock.swift index a1aa227d..f38fd345 100644 --- a/GravatarAppTests/Helpers/URLSessionMock.swift +++ b/GravatarAppTests/Helpers/URLSessionMock.swift @@ -41,7 +41,9 @@ final class URLSessionMock: URLSessionProtocol, @unchecked Sendable { if request.isProfilesRequest { return (Bundle.fullProfileJsonData, HTTPURLResponse.successResponse()) // Profile data - } else if request.isAvatarsRequest == true { + } + + if request.isAvatarsRequest == true { if shouldFetchEmptyAvatarsGrid { if let data = "[]".data(using: .utf8) { return (data, HTTPURLResponse.successResponse()) @@ -51,6 +53,10 @@ final class URLSessionMock: URLSessionProtocol, @unchecked Sendable { } } + if request.isGetAvatarRequest { + return (ImageHelper.testImageData, HTTPURLResponse.successResponse()) + } + fatalError("Request not mocked: \(request.url?.absoluteString ?? "unknown request")") } @@ -110,6 +116,17 @@ extension URLRequest { } return self.httpBody.contains("email_hash") } + + fileprivate var isGetAvatarRequest: Bool { + guard + httpMethod == "GET", + value(forHTTPHeaderField: "Accept")?.contains("image/*") == true, + url?.absoluteString.contains("avatar") == true, + httpBody == nil + else { return false } + + return true + } } extension Data? { diff --git a/GravatarAppTests/Helpers/UserDefaults+Extension.swift b/GravatarAppTests/Helpers/UserDefaults+Extension.swift index e2f07e52..262810ee 100644 --- a/GravatarAppTests/Helpers/UserDefaults+Extension.swift +++ b/GravatarAppTests/Helpers/UserDefaults+Extension.swift @@ -4,7 +4,11 @@ extension UserDefaults { static let testSuiteName = "test.GravatarApp" static let testUserDefaults = UserDefaults(suiteName: testSuiteName)! - static func deleteTestData() { - UserDefaults.testUserDefaults.removePersistentDomain(forName: UserDefaults.testSuiteName) + static func deleteTestData(named name: String = testSuiteName) { + UserDefaults.testUserDefaults.removePersistentDomain(forName: name) + } + + static func testUserDefaults(named name: String) -> UserDefaults { + UserDefaults(suiteName: name)! } } diff --git a/GravatarAppTests/ShareScreenTests/ShareViewModelTests.swift b/GravatarAppTests/ShareScreenTests/ShareViewModelTests.swift new file mode 100644 index 00000000..f307c127 --- /dev/null +++ b/GravatarAppTests/ShareScreenTests/ShareViewModelTests.swift @@ -0,0 +1,138 @@ +import Foundation +import Gravatar +@testable import GravatarApp +import SnapshotTesting +import Testing + +@MainActor +@Suite(.snapshots(record: .failed, diffTool: .ksdiff)) +struct ShareViewModelTests { + let networkMonitor = TestNetworkMonitor() + let urlSession = URLSessionMock() + + @Test("Tets the vcard share link URL is generated with the correct data") + func shareVCardWithFullData() async throws { + let viewModel = createViewModel() + + await viewModel.shareVCard() + + #expect(viewModel.shareVCardURL != nil) + + let vCard = try String(contentsOf: viewModel.shareVCardURL!, encoding: .utf8) + print("Stored data 1: \(String(describing: UserDefaults.testUserDefaults.value(forKey: "https://notreal.wordpress.com") as? Bool))") + + #expect(vCard.contains("N:Appleseed;John;")) + #expect(vCard.contains("FN:\n")) // Always empty + #expect(vCard.contains("NICKNAME:John Appleseed")) + #expect(vCard.contains("ORG:A company")) + #expect(vCard.contains("TITLE:Engineer")) + #expect(vCard.contains("URL:https://gravatar.com/notreal")) + #expect(vCard.contains("ADR;CHARSET=UTF-8;TYPE=HOME:;;;Atlanta GA;;;")) + #expect(vCard.contains("URL;TYPE=\"WordPress\":https://notreal.wordpress.com")) + #expect(vCard.contains("NOTE:I'm a ")) + #expect(vCard.contains("EMAIL:notreal@example.com")) + #expect(vCard.contains("TEL:+1234567890")) + #expect(vCard.contains("PHOTO;ENCODING=b;TYPE=JPEG:/9j/4")) + + UserDefaults.deleteTestData(named: #function) + } + + @Test("Tets the vcard share link URL is generated with the correct data when no data is shared") + func shareVCardWithNoData() async throws { + let viewModel = createViewModel() + viewModel.share.email = false + viewModel.share.phone = false + viewModel.share.name = false + viewModel.share.location = false + viewModel.share.jobTitle = false + viewModel.share.company = false + viewModel.share.description = false + viewModel.share.profileURL = false + viewModel.share.set(verifiedAccount, to: false) + + await viewModel.shareVCard() + + #expect(viewModel.shareVCardURL != nil) + + let vCard = try String(contentsOf: viewModel.shareVCardURL!, encoding: .utf8) + + // Expected + #expect(vCard.contains("FN:\n")) // Always empty + #expect(vCard.contains("NICKNAME:John Appleseed")) + #expect(vCard.contains("PHOTO;ENCODING=b;TYPE=JPEG:/9j/4")) + // Not expected + #expect(!vCard.contains("N:Appleseed;John;")) + #expect(!vCard.contains("ORG:A company")) + #expect(!vCard.contains("TITLE:Engineer")) + #expect(!vCard.contains("URL:https://gravatar.com/notreal")) + #expect(!vCard.contains("ADR;CHARSET=UTF-8;TYPE=HOME:;;;Atlanta GA;;;")) + #expect(!vCard.contains("URL;TYPE=\"WordPress\":https://notreal.wordpress.com")) + #expect(!vCard.contains("NOTE:I'm a ")) + #expect(!vCard.contains("EMAIL:notreal@example.com")) + #expect(!vCard.contains("TEL:+1234567890")) + + UserDefaults.deleteTestData(named: #function) + } + + @Test("Test the qr code is generated correctly with all data") + func qrCodeGeneratedFullData() async throws { + let viewModel = createViewModel() + + await viewModel.generateVCardQR() + + #expect(viewModel.qrCodeImage != nil) + let image = viewModel.qrCodeImage!.resizable().frame(width: 100, height: 100) + + assertSnapshots(of: image, as: [.image]) + + UserDefaults.deleteTestData(named: #function) + } + + @Test("Test the qr code is generated correctly with minimal data") + func qrCodeGeneratedMinimalData() async throws { + let viewModel = createViewModel() + + viewModel.share.email = false + viewModel.share.phone = false + viewModel.share.name = false + viewModel.share.location = false + viewModel.share.jobTitle = false + viewModel.share.company = false + viewModel.share.description = false + viewModel.share.profileURL = false + viewModel.share.set(verifiedAccount, to: false) + + await viewModel.generateVCardQR() + + #expect(viewModel.qrCodeImage != nil) + let image = viewModel.qrCodeImage!.resizable().frame(width: 100, height: 100) + + assertSnapshots(of: image, as: [.image]) + + UserDefaults.deleteTestData(named: #function) + } +} + +@MainActor +private func createViewModel(_ testUnitName: String = #function) -> ShareViewModel { + let viewModel = ShareViewModel( + userSession: UserSession( + profile: .full, + accessToken: "", + context: .testContext, + networkMonitor: TestNetworkMonitor(), + urlSession: URLSessionMock() + ), + urlSession: URLSessionMock(), + networkMonitor: TestNetworkMonitor(), + // These tests run in parallel, so we need different UserDefaults for each one of them. + userDefaults: .testUserDefaults(named: testUnitName) + ) + viewModel.storedUserEmail = "notreal@example.com" + viewModel.storedPhoneNumber = "+1234567890" + return viewModel +} + +private var verifiedAccount: VerifiedAccount { + Profile.full.verifiedAccounts[0] +} diff --git a/GravatarAppTests/ShareScreenTests/__Snapshots__/ShareViewModelTests/qrCodeGeneratedFullData.1.png b/GravatarAppTests/ShareScreenTests/__Snapshots__/ShareViewModelTests/qrCodeGeneratedFullData.1.png new file mode 100644 index 00000000..e2ccc37a Binary files /dev/null and b/GravatarAppTests/ShareScreenTests/__Snapshots__/ShareViewModelTests/qrCodeGeneratedFullData.1.png differ diff --git a/GravatarAppTests/ShareScreenTests/__Snapshots__/ShareViewModelTests/qrCodeGeneratedMinimalData.1.png b/GravatarAppTests/ShareScreenTests/__Snapshots__/ShareViewModelTests/qrCodeGeneratedMinimalData.1.png new file mode 100644 index 00000000..56fa2d8d Binary files /dev/null and b/GravatarAppTests/ShareScreenTests/__Snapshots__/ShareViewModelTests/qrCodeGeneratedMinimalData.1.png differ