Settings previously only surfaced an error message after a failed Test Connection attempt — there was no way to trigger the actual macOS Automation permission dialog or see live grant/deny status, unlike Calendar/Contacts/Reminders/Location. AEDeterminePermissionToAutomateTarget (askUserIfNeeded: false/true) turns out to provide exactly that: a non-prompting status check and an explicit prompt-and-wait call, mirroring EKEventStore.authorizationStatus(for:)/ requestFullAccessToEvents(). Mail's Settings row now uses the same personalDataRow component as Calendar/Contacts — live status badge plus a real "Request Access" button — instead of a one-off Test-Connection-only UI. Test Connection stays as a secondary functional check.
192 lines
7.9 KiB
Swift
192 lines
7.9 KiB
Swift
//
|
|
// AppleMailServiceTests.swift
|
|
// oAITests
|
|
//
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
|
// Copyright (C) 2026 Rune Olsen
|
|
|
|
import Testing
|
|
import Foundation
|
|
@testable import Confab
|
|
|
|
@Suite("AppleMailService descriptor decoding and formatting")
|
|
struct AppleMailServiceTests {
|
|
|
|
// MARK: - decodeStringList
|
|
|
|
private func makeListDescriptor(_ items: [String]) -> NSAppleEventDescriptor {
|
|
let list = NSAppleEventDescriptor.list()
|
|
for (index, item) in items.enumerated() {
|
|
list.insert(NSAppleEventDescriptor(string: item), at: index + 1)
|
|
}
|
|
return list
|
|
}
|
|
|
|
@Test("decodeStringList decodes a flat list of strings")
|
|
func decodesFlatList() {
|
|
let descriptor = makeListDescriptor(["a", "b", "c"])
|
|
#expect(AppleMailService.decodeStringList(descriptor) == ["a", "b", "c"])
|
|
}
|
|
|
|
@Test("decodeStringList returns empty array for an empty list")
|
|
func decodesEmptyList() {
|
|
let descriptor = makeListDescriptor([])
|
|
#expect(AppleMailService.decodeStringList(descriptor) == [])
|
|
}
|
|
|
|
@Test("decodeStringList falls back to a single-element array for a bare string descriptor")
|
|
func decodesBareString() {
|
|
let descriptor = NSAppleEventDescriptor(string: "hello")
|
|
#expect(AppleMailService.decodeStringList(descriptor) == ["hello"])
|
|
}
|
|
|
|
// MARK: - parseSearchResults
|
|
|
|
@Test("parseSearchResults decodes a list of 7-field message records")
|
|
func parsesSearchResults() {
|
|
let record1 = makeListDescriptor(["msg-1", "Receipt from Elkjøp", "orders@elkjop.no", "Monday, 1 January 2026", "INBOX", "Personal", "1"])
|
|
let record2 = makeListDescriptor(["msg-2", "Another subject", "someone@example.com", "Tuesday, 2 January 2026", "INBOX", "Work", "0"])
|
|
let outer = NSAppleEventDescriptor.list()
|
|
outer.insert(record1, at: 1)
|
|
outer.insert(record2, at: 2)
|
|
|
|
let results = AppleMailService.parseSearchResults(from: outer)
|
|
#expect(results.count == 2)
|
|
#expect(results[0].messageId == "msg-1")
|
|
#expect(results[0].subject == "Receipt from Elkjøp")
|
|
#expect(results[0].from == "orders@elkjop.no")
|
|
#expect(results[0].mailbox == "INBOX")
|
|
#expect(results[0].account == "Personal")
|
|
#expect(results[0].attachmentCount == "1")
|
|
#expect(results[1].messageId == "msg-2")
|
|
#expect(results[1].attachmentCount == "0")
|
|
}
|
|
|
|
@Test("parseSearchResults returns empty array for an empty outer list")
|
|
func parsesEmptySearchResults() {
|
|
let outer = NSAppleEventDescriptor.list()
|
|
#expect(AppleMailService.parseSearchResults(from: outer) == [])
|
|
}
|
|
|
|
@Test("parseSearchResults skips malformed records with too few fields")
|
|
func skipsMalformedRecords() {
|
|
let goodRecord = makeListDescriptor(["msg-1", "Subject", "from@example.com", "date", "INBOX", "Account", "0"])
|
|
let badRecord = makeListDescriptor(["only", "three", "fields"])
|
|
let outer = NSAppleEventDescriptor.list()
|
|
outer.insert(badRecord, at: 1)
|
|
outer.insert(goodRecord, at: 2)
|
|
|
|
let results = AppleMailService.parseSearchResults(from: outer)
|
|
#expect(results.count == 1)
|
|
#expect(results[0].messageId == "msg-1")
|
|
}
|
|
|
|
// MARK: - mapAppleScriptError
|
|
|
|
@Test("mapAppleScriptError maps -1743 to an Automation-permission message")
|
|
func mapsNotAuthorizedError() {
|
|
let message = AppleMailService.mapAppleScriptError(number: -1743, message: "Not authorized")
|
|
#expect(message.contains("not authorized"))
|
|
#expect(message.contains("Automation"))
|
|
}
|
|
|
|
@Test("mapAppleScriptError maps -600 to a Mail-not-available message")
|
|
func mapsApplicationNotRunningError() {
|
|
let message = AppleMailService.mapAppleScriptError(number: -600, message: "Application isn't running")
|
|
#expect(message.contains("not available"))
|
|
}
|
|
|
|
@Test("mapAppleScriptError falls back to a generic formatted message for unknown error numbers")
|
|
func mapsGenericError() {
|
|
let message = AppleMailService.mapAppleScriptError(number: -1234, message: "Something odd happened")
|
|
#expect(message.contains("-1234"))
|
|
#expect(message.contains("Something odd happened"))
|
|
}
|
|
|
|
// MARK: - parseArgs
|
|
|
|
@Test("parseArgs decodes a JSON arguments string")
|
|
func parsesArgsJSON() {
|
|
let args = AppleMailService.parseArgs("{\"account\":\"work@example.com\",\"subject_contains\":\"Elkjøp\"}")
|
|
#expect(args["account"] as? String == "work@example.com")
|
|
#expect(args["subject_contains"] as? String == "Elkjøp")
|
|
}
|
|
|
|
@Test("parseArgs returns empty dictionary for invalid JSON")
|
|
func parsesInvalidArgsJSON() {
|
|
let args = AppleMailService.parseArgs("not json")
|
|
#expect(args.isEmpty)
|
|
}
|
|
|
|
// MARK: - hasSearchCriteria
|
|
|
|
@Test("hasSearchCriteria is false when every criterion is nil or empty")
|
|
func rejectsUnscopedSearch() {
|
|
#expect(AppleMailService.hasSearchCriteria(subjectContains: nil, fromContains: "", bodyContains: nil, since: nil, before: "") == false)
|
|
}
|
|
|
|
@Test("hasSearchCriteria is true when any single criterion is non-empty")
|
|
func acceptsScopedSearch() {
|
|
#expect(AppleMailService.hasSearchCriteria(subjectContains: "Elkjøp", fromContains: nil, bodyContains: nil, since: nil, before: nil) == true)
|
|
#expect(AppleMailService.hasSearchCriteria(subjectContains: nil, fromContains: nil, bodyContains: nil, since: "2026-01-01", before: nil) == true)
|
|
}
|
|
|
|
// MARK: - approvalSummary
|
|
|
|
@Test("approvalSummary describes mail_list_accounts")
|
|
func summarizesListAccounts() {
|
|
let summary = AppleMailService.shared.approvalSummary(forTool: "mail_list_accounts", arguments: "{}")
|
|
#expect(summary == "List your Mail.app accounts")
|
|
}
|
|
|
|
@Test("approvalSummary includes account and subject scope for mail_search")
|
|
func summarizesSearch() {
|
|
let summary = AppleMailService.shared.approvalSummary(
|
|
forTool: "mail_search",
|
|
arguments: "{\"account\":\"work@example.com\",\"subject_contains\":\"Elkjøp\"}"
|
|
)
|
|
#expect(summary.contains("work@example.com"))
|
|
#expect(summary.contains("Elkjøp"))
|
|
}
|
|
|
|
@Test("approvalSummary for mail_search with no scope still returns a sensible label")
|
|
func summarizesUnscopedSearch() {
|
|
let summary = AppleMailService.shared.approvalSummary(forTool: "mail_search", arguments: "{}")
|
|
#expect(summary == "Search Mail")
|
|
}
|
|
|
|
@Test("approvalSummary describes mail_save_attachment with name and destination")
|
|
func summarizesSaveAttachment() {
|
|
let summary = AppleMailService.shared.approvalSummary(
|
|
forTool: "mail_save_attachment",
|
|
arguments: "{\"attachment_name\":\"receipt.pdf\",\"dest_path\":\"/tmp/receipt.pdf\"}"
|
|
)
|
|
#expect(summary.contains("receipt.pdf"))
|
|
#expect(summary.contains("/tmp/receipt.pdf"))
|
|
}
|
|
|
|
@Test("approvalSummary falls back to a generic label for an unknown tool name")
|
|
func summarizesUnknownTool() {
|
|
let summary = AppleMailService.shared.approvalSummary(forTool: "mail_bogus", arguments: "{}")
|
|
#expect(summary == "Perform action: mail_bogus")
|
|
}
|
|
|
|
// MARK: - mapAutomationPermissionStatus
|
|
|
|
@Test("mapAutomationPermissionStatus maps noErr (0) to granted")
|
|
func mapsGrantedStatus() {
|
|
#expect(AppleMailService.mapAutomationPermissionStatus(0) == .granted)
|
|
}
|
|
|
|
@Test("mapAutomationPermissionStatus maps -1743 to denied")
|
|
func mapsDeniedStatus() {
|
|
#expect(AppleMailService.mapAutomationPermissionStatus(-1743) == .denied)
|
|
}
|
|
|
|
@Test("mapAutomationPermissionStatus maps -1744 (would require consent) and other codes to notDetermined")
|
|
func mapsNotDeterminedStatus() {
|
|
#expect(AppleMailService.mapAutomationPermissionStatus(-1744) == .notDetermined)
|
|
#expect(AppleMailService.mapAutomationPermissionStatus(-9999) == .notDetermined)
|
|
}
|
|
}
|