Skip to content
This repository was archived by the owner on Aug 21, 2025. It is now read-only.
Merged
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
16 changes: 16 additions & 0 deletions GravatarApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Comment on lines +189 to 191
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was stubbornly appearing every time. Probably from #96

mainGroup = 1EDD7F872DF9B94300E5F1B6;
minimizedProjectReferenceProxies = 1;
Expand Down
4 changes: 2 additions & 2 deletions GravatarApp/Share/ShareViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down Expand Up @@ -57,7 +57,7 @@ class ShareViewModel: ObservableObject {
}
}

func setupObservers() {
private func setupObservers() {
userSession.$profile
.receive(on: RunLoop.main)
.sink { [weak self] newProfile in
Expand Down
2 changes: 1 addition & 1 deletion GravatarAppTests/Helpers/ImageHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion GravatarAppTests/Helpers/URLSessionMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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")")
}

Expand Down Expand Up @@ -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? {
Expand Down
8 changes: 6 additions & 2 deletions GravatarAppTests/Helpers/UserDefaults+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)!
}
}
138 changes: 138 additions & 0 deletions GravatarAppTests/ShareScreenTests/ShareViewModelTests.swift
Original file line number Diff line number Diff line change
@@ -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]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.