405 lines
15 KiB
Swift
405 lines
15 KiB
Swift
import Foundation
|
|
import Testing
|
|
import SwiftData
|
|
@testable import BusinessCard
|
|
|
|
@MainActor
|
|
struct BusinessCardTests {
|
|
|
|
private func makeTestContainer() throws -> ModelContainer {
|
|
let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
|
return try ModelContainer(for: BusinessCard.self, Contact.self, AppSettings.self, configurations: config)
|
|
}
|
|
|
|
@Test func vCardPayloadIncludesFields() async throws {
|
|
let container = try makeTestContainer()
|
|
let context = container.mainContext
|
|
|
|
let card = BusinessCard(
|
|
displayName: "Test User",
|
|
role: "Developer",
|
|
company: "Test Corp",
|
|
bio: "A passionate developer"
|
|
)
|
|
context.insert(card)
|
|
|
|
// Add contact fields using the ContactField relationship
|
|
card.addContactField(.email, value: "test@example.com", title: "Work")
|
|
card.addContactField(.phone, value: "+1 555 123 4567", title: "Cell")
|
|
card.addContactField(.website, value: "example.com", title: "")
|
|
|
|
#expect(card.vCardPayload.contains("BEGIN:VCARD"))
|
|
#expect(card.vCardPayload.contains("FN:Test User"))
|
|
#expect(card.vCardPayload.contains("ORG:Test Corp"))
|
|
#expect(card.vCardPayload.contains("EMAIL;TYPE=WORK:test@example.com"))
|
|
#expect(card.vCardPayload.contains("TEL;TYPE=CELL:+1 555 123 4567"))
|
|
#expect(card.vCardPayload.contains("A passionate developer"))
|
|
#expect(card.vCardPayload.contains("END:VCARD"))
|
|
}
|
|
|
|
@Test func vCardPayloadIncludesStructuredName() async throws {
|
|
let container = try makeTestContainer()
|
|
let context = container.mainContext
|
|
|
|
let card = BusinessCard(
|
|
prefix: "Dr.",
|
|
firstName: "John",
|
|
middleName: "Michael",
|
|
lastName: "Smith",
|
|
suffix: "Jr."
|
|
)
|
|
context.insert(card)
|
|
|
|
// N: LastName;FirstName;MiddleName;Prefix;Suffix
|
|
#expect(card.vCardPayload.contains("N:Smith;John;Michael;Dr.;Jr."))
|
|
#expect(card.vCardPayload.contains("FN:Dr. John Michael Smith Jr."))
|
|
}
|
|
|
|
@Test func vCardPayloadIncludesAllContactInfo() async throws {
|
|
let container = try makeTestContainer()
|
|
let context = container.mainContext
|
|
|
|
let card = BusinessCard(
|
|
role: "CEO",
|
|
company: "Acme Inc",
|
|
prefix: "",
|
|
firstName: "Jane",
|
|
lastName: "Doe",
|
|
pronouns: "she/her",
|
|
department: "Executive",
|
|
headline: "Building the future",
|
|
bio: "Passionate leader",
|
|
accreditations: "MBA, CPA"
|
|
)
|
|
context.insert(card)
|
|
|
|
// Verify structured name
|
|
#expect(card.vCardPayload.contains("N:Doe;Jane;;;"))
|
|
#expect(card.vCardPayload.contains("FN:Jane Doe"))
|
|
|
|
// Verify org with department
|
|
#expect(card.vCardPayload.contains("ORG:Acme Inc;Executive"))
|
|
|
|
// Verify title
|
|
#expect(card.vCardPayload.contains("TITLE:CEO"))
|
|
|
|
// Verify note contains all info
|
|
#expect(card.vCardPayload.contains("Building the future"))
|
|
#expect(card.vCardPayload.contains("Passionate leader"))
|
|
#expect(card.vCardPayload.contains("Credentials: MBA\\, CPA"))
|
|
#expect(card.vCardPayload.contains("Pronouns: she/her"))
|
|
}
|
|
|
|
@Test func defaultCardSelectionUpdatesCards() async throws {
|
|
let container = try makeTestContainer()
|
|
let context = container.mainContext
|
|
|
|
// Insert cards directly instead of using samples (which might trigger other logic)
|
|
let card1 = BusinessCard(role: "Role", company: "Company", isDefault: true, firstName: "Card", lastName: "One")
|
|
let card2 = BusinessCard(role: "Role", company: "Company", isDefault: false, firstName: "Card", lastName: "Two")
|
|
context.insert(card1)
|
|
context.insert(card2)
|
|
try context.save()
|
|
|
|
let store = CardStore(modelContext: context)
|
|
|
|
#expect(store.cards.count >= 2)
|
|
|
|
store.setDefaultCard(card2)
|
|
|
|
#expect(store.selectedCardID == card2.id)
|
|
#expect(store.cards.filter { $0.isDefault }.count == 1)
|
|
#expect(store.cards.first { $0.isDefault }?.id == card2.id)
|
|
}
|
|
|
|
@Test func contactsSearchFiltersByNameOrCompany() async throws {
|
|
let container = try makeTestContainer()
|
|
let context = container.mainContext
|
|
|
|
// Insert contacts directly
|
|
let contact1 = Contact(name: "John Doe", role: "Developer", company: "Global Bank")
|
|
let contact2 = Contact(name: "Jane Smith", role: "Designer", company: "Tech Corp")
|
|
context.insert(contact1)
|
|
context.insert(contact2)
|
|
try context.save()
|
|
|
|
// Create store without triggering sample creation - just use the context
|
|
let descriptor = FetchDescriptor<Contact>()
|
|
let contacts = try context.fetch(descriptor)
|
|
|
|
// Filter manually to test the logic
|
|
let query = "Global"
|
|
let filtered = contacts.filter { $0.company.localizedStandardContains(query) }
|
|
|
|
#expect(filtered.count == 1)
|
|
#expect(filtered.first?.company == "Global Bank")
|
|
}
|
|
|
|
@Test func addCardIncreasesCardCount() async throws {
|
|
let container = try makeTestContainer()
|
|
let context = container.mainContext
|
|
|
|
let store = CardStore(modelContext: context)
|
|
let initialCount = store.cards.count
|
|
|
|
let newCard = BusinessCard(
|
|
role: "Manager",
|
|
company: "New Corp",
|
|
firstName: "New",
|
|
lastName: "User"
|
|
)
|
|
store.addCard(newCard)
|
|
|
|
#expect(store.cards.count == initialCount + 1)
|
|
}
|
|
|
|
@Test func deleteCardRemovesFromStore() async throws {
|
|
let container = try makeTestContainer()
|
|
let context = container.mainContext
|
|
|
|
let card1 = BusinessCard(role: "Role", company: "Company", firstName: "Card", lastName: "One")
|
|
let card2 = BusinessCard(role: "Role", company: "Company", firstName: "Card", lastName: "Two")
|
|
context.insert(card1)
|
|
context.insert(card2)
|
|
try context.save()
|
|
|
|
let store = CardStore(modelContext: context)
|
|
let initialCount = store.cards.count
|
|
|
|
store.deleteCard(card2)
|
|
|
|
#expect(store.cards.count == initialCount - 1)
|
|
#expect(!store.cards.contains(where: { $0.id == card2.id }))
|
|
}
|
|
|
|
@Test func updateCardChangesProperties() async throws {
|
|
let container = try makeTestContainer()
|
|
let context = container.mainContext
|
|
|
|
let card = BusinessCard(
|
|
displayName: "Original Name",
|
|
role: "Original Role",
|
|
company: "Original Company"
|
|
)
|
|
context.insert(card)
|
|
try context.save()
|
|
|
|
let store = CardStore(modelContext: context)
|
|
|
|
card.firstName = "Updated"
|
|
card.lastName = "Name"
|
|
card.role = "Updated Role"
|
|
store.updateCard(card)
|
|
|
|
let updatedCard = store.cards.first(where: { $0.id == card.id })
|
|
#expect(updatedCard?.fullName == "Updated Name")
|
|
#expect(updatedCard?.role == "Updated Role")
|
|
}
|
|
|
|
@Test func recordShareCreatesContact() async throws {
|
|
let container = try makeTestContainer()
|
|
let context = container.mainContext
|
|
|
|
// Manually insert a contact and test recordShare logic
|
|
let newContact = Contact(
|
|
name: "New Contact",
|
|
role: "CEO",
|
|
company: "Partner Inc",
|
|
cardLabel: "Work"
|
|
)
|
|
context.insert(newContact)
|
|
try context.save()
|
|
|
|
let descriptor = FetchDescriptor<Contact>()
|
|
let contacts = try context.fetch(descriptor)
|
|
|
|
#expect(contacts.count >= 1)
|
|
#expect(contacts.first(where: { $0.name == "New Contact" }) != nil)
|
|
}
|
|
|
|
@Test func recordShareUpdatesExistingContact() async throws {
|
|
let container = try makeTestContainer()
|
|
let context = container.mainContext
|
|
|
|
let existingContact = Contact(
|
|
name: "Existing Contact",
|
|
role: "Manager",
|
|
company: "Partner Inc",
|
|
cardLabel: "Personal"
|
|
)
|
|
context.insert(existingContact)
|
|
try context.save()
|
|
|
|
// Update the contact
|
|
existingContact.cardLabel = "Work"
|
|
existingContact.lastSharedDate = .now
|
|
try context.save()
|
|
|
|
let descriptor = FetchDescriptor<Contact>()
|
|
let contacts = try context.fetch(descriptor)
|
|
|
|
let updated = contacts.first(where: { $0.name == "Existing Contact" })
|
|
#expect(updated?.cardLabel == "Work")
|
|
}
|
|
|
|
@Test func themeAssignmentWorks() async throws {
|
|
let card = BusinessCard()
|
|
|
|
card.theme = .midnight
|
|
#expect(card.themeName == "Midnight")
|
|
#expect(card.theme == .midnight)
|
|
|
|
card.theme = .ocean
|
|
#expect(card.themeName == "Ocean")
|
|
}
|
|
|
|
@Test func layoutStyleAssignmentWorks() async throws {
|
|
let card = BusinessCard()
|
|
|
|
card.layoutStyle = .split
|
|
#expect(card.layoutStyleRawValue == "split")
|
|
#expect(card.layoutStyle == .split)
|
|
|
|
card.layoutStyle = .photo
|
|
#expect(card.layoutStyleRawValue == "photo")
|
|
}
|
|
|
|
// Tests for high priority features
|
|
|
|
@Test func contactNotesAndTags() async throws {
|
|
let contact = Contact(
|
|
name: "Test Contact",
|
|
notes: "Met at conference",
|
|
tags: "VIP, client, tech"
|
|
)
|
|
|
|
#expect(contact.tagList.count == 3)
|
|
#expect(contact.tagList.contains("VIP"))
|
|
#expect(contact.tagList.contains("client"))
|
|
#expect(contact.tagList.contains("tech"))
|
|
}
|
|
|
|
@Test func contactFollowUpStatus() async throws {
|
|
let contact = Contact(name: "Test")
|
|
#expect(!contact.hasFollowUp)
|
|
#expect(!contact.isFollowUpOverdue)
|
|
|
|
contact.followUpDate = .now.addingTimeInterval(86400) // Tomorrow
|
|
#expect(contact.hasFollowUp)
|
|
#expect(!contact.isFollowUpOverdue)
|
|
|
|
contact.followUpDate = .now.addingTimeInterval(-86400) // Yesterday
|
|
#expect(contact.isFollowUpOverdue)
|
|
}
|
|
|
|
@Test func contactFromVCardParsing() async throws {
|
|
let vCardData = """
|
|
BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:John Smith
|
|
ORG:Acme Corp
|
|
TITLE:CEO
|
|
EMAIL;TYPE=work:john@acme.com
|
|
TEL;TYPE=work:+1 555 123 4567
|
|
END:VCARD
|
|
"""
|
|
|
|
let contact = Contact.fromVCard(vCardData)
|
|
|
|
#expect(contact.name == "John Smith")
|
|
#expect(contact.company == "Acme Corp")
|
|
#expect(contact.role == "CEO")
|
|
#expect(contact.email == "john@acme.com")
|
|
#expect(contact.phone == "+1 555 123 4567")
|
|
#expect(contact.isReceivedCard)
|
|
#expect(contact.cardLabel == "Received")
|
|
}
|
|
|
|
@Test func addReceivedCardFromVCard() async throws {
|
|
let container = try makeTestContainer()
|
|
let context = container.mainContext
|
|
|
|
let vCardData = """
|
|
BEGIN:VCARD
|
|
VERSION:3.0
|
|
FN:Jane Doe
|
|
ORG:Test Inc
|
|
END:VCARD
|
|
"""
|
|
|
|
let contact = Contact.fromVCard(vCardData)
|
|
context.insert(contact)
|
|
try context.save()
|
|
|
|
let descriptor = FetchDescriptor<Contact>()
|
|
let contacts = try context.fetch(descriptor)
|
|
|
|
let received = contacts.first(where: { $0.name == "Jane Doe" })
|
|
#expect(received != nil)
|
|
#expect(received?.isReceivedCard == true)
|
|
}
|
|
|
|
@Test func addedContactFieldShortDisplayFormatsPhone() async throws {
|
|
let field = AddedContactField(fieldType: .phone, value: "2145559898", title: "Cell")
|
|
#expect(field.shortDisplayValue == "(214) 555-9898")
|
|
}
|
|
|
|
@Test func addedContactFieldShortDisplayKeepsAddressSingleLine() async throws {
|
|
let address = PostalAddress(
|
|
street: "123 Main St",
|
|
city: "Plano",
|
|
state: "TX",
|
|
postalCode: "75024"
|
|
)
|
|
let field = AddedContactField(fieldType: .address, value: address.encode(), title: "Work")
|
|
|
|
#expect(field.shortDisplayValue == "123 Main St, Plano, TX, 75024")
|
|
}
|
|
|
|
@Test func contactFieldRulesNormalizeWebsiteWithoutScheme() async throws {
|
|
let value = "company.com/team"
|
|
#expect(ContactFieldInputRules.isValid(value, for: "website"))
|
|
#expect(ContactFieldInputRules.normalizedForStorage(value, for: "website") == "https://company.com/team")
|
|
}
|
|
|
|
@Test func contactFieldRulesValidateSocialUsernameOrLink() async throws {
|
|
#expect(ContactFieldInputRules.isValid("mattbruce", for: "linkedIn"))
|
|
#expect(ContactFieldInputRules.isValid("linkedin.com/in/mattbruce", for: "linkedIn"))
|
|
#expect(
|
|
ContactFieldInputRules.normalizedForStorage("mattbruce", for: "linkedIn")
|
|
== "https://www.linkedin.com/in/mattbruce/"
|
|
)
|
|
#expect(
|
|
ContactFieldInputRules.normalizedForStorage("https://linkedin.com/in/mattbruce", for: "linkedIn")
|
|
== "https://www.linkedin.com/in/mattbruce/"
|
|
)
|
|
#expect(!ContactFieldInputRules.isValid("https://www.linkedin.com/company/acme", for: "linkedIn"))
|
|
#expect(!ContactFieldInputRules.isValid("bad value with spaces", for: "linkedIn"))
|
|
}
|
|
|
|
@Test func contactFieldRulesValidateMessagingPhoneFields() async throws {
|
|
#expect(ContactFieldInputRules.isValid("+1 214 555 9898", for: "whatsapp"))
|
|
#expect(ContactFieldInputRules.normalizedForStorage("+1 214 555 9898", for: "signal") == "+12145559898")
|
|
#expect(!ContactFieldInputRules.isValid("123", for: "whatsapp"))
|
|
}
|
|
|
|
@Test func contactFieldRulesValidatePaymentFields() async throws {
|
|
#expect(ContactFieldInputRules.isValid("$mattbruce", for: "cashApp"))
|
|
#expect(!ContactFieldInputRules.isValid("$bad tag", for: "cashApp"))
|
|
#expect(ContactFieldInputRules.isValid("matt@example.com", for: "zelle"))
|
|
#expect(ContactFieldInputRules.isValid("+1 214 555 9898", for: "zelle"))
|
|
#expect(ContactFieldInputRules.isValid("paypal.me/mattbruce", for: "paypal"))
|
|
}
|
|
|
|
@Test func contactFieldTypeBuildersUseServiceCorrectLinks() async throws {
|
|
#expect(ContactFieldType.twitter.buildURL(value: "mattbruce")?.absoluteString == "https://x.com/mattbruce")
|
|
#expect(ContactFieldType.snapchat.buildURL(value: "mattbruce")?.absoluteString == "https://www.snapchat.com/add/mattbruce")
|
|
#expect(ContactFieldType.tiktok.buildURL(value: "mattbruce")?.absoluteString == "https://www.tiktok.com/@mattbruce")
|
|
#expect(ContactFieldType.threads.buildURL(value: "mattbruce")?.absoluteString == "https://www.threads.net/@mattbruce")
|
|
#expect(ContactFieldType.reddit.buildURL(value: "mattbruce")?.absoluteString == "https://www.reddit.com/user/mattbruce")
|
|
#expect(ContactFieldType.telegram.buildURL(value: "@mattbruce")?.absoluteString == "https://t.me/mattbruce")
|
|
#expect(ContactFieldType.venmo.buildURL(value: "@mattbruce")?.absoluteString == "https://venmo.com/u/mattbruce")
|
|
#expect(ContactFieldType.cashApp.buildURL(value: "$mattbruce")?.absoluteString == "https://cash.app/$mattbruce")
|
|
}
|
|
}
|