import Foundation import Testing @testable import MikeMail @MainActor struct MailIdleTests { @Test func classifiesServerResponses() { #expect(IMAPClient.isMailboxChange("* 12 EXISTS")) #expect(IMAPClient.isMailboxChange("* 2 EXPUNGE")) #expect(IMAPClient.isMailboxChange("* 2 FETCH (FLAGS (\\Seen))")) #expect(!IMAPClient.isMailboxChange("* OK Still here")) #expect(!IMAPClient.isMailboxChange("M4 OK IDLE completed")) #expect(IMAPClient.isAuthenticationFailure("M1 NO [AUTHENTICATIONFAILED] Authentication failed.")) #expect(IMAPClient.isAuthenticationFailure("M1 NO LOGIN failed")) #expect(!IMAPClient.isAuthenticationFailure("M1 NO [UNAVAILABLE] Server temporarily unavailable")) #expect(!IMAPClient.isAuthenticationFailure("* BYE Connection closed")) } @Test func idleChangesTrackSequenceNumbersWithoutRefreshing() { var mailbox = IMAPClient.IdleMailbox(validity: "7", uids: [10, 20, 30]) #expect(mailbox.receive("* 2 FETCH (FLAGS (\\Seen))") == .flags(validity: "7", uid: 20, flags: ["\\Seen"])) #expect(mailbox.receive("* 2 EXPUNGE") == .removed(validity: "7", uid: 20)) #expect(mailbox.receive("* 2 EXPUNGE") == .removed(validity: "7", uid: 30)) #expect(mailbox.receive("* 1 EXISTS") == nil) #expect(mailbox.receive("* 2 EXISTS") == .refresh) #expect(mailbox.receive("* 2 FETCH (UID 40 FLAGS ())") == .flags(validity: "7", uid: 40, flags: [])) #expect(mailbox.receive("* 2 EXPUNGE") == .removed(validity: "7", uid: 40)) #expect(mailbox.receive("* 9 EXPUNGE") == .refresh) #expect(mailbox.receive("* OK Still here") == nil) } @Test func idleEchoAfterMoveOrDeletionDoesNotReloadOrReviveSource() throws { let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) defer { try? FileManager.default.removeItem(at: root) } let archive = try MailArchive(root: root) let account = MailAccount(name: "A") let inbox = MailFolder(accountID: account.id, path: "INBOX", name: "Inbox") let trash = MailFolder(accountID: account.id, path: "Trash", name: "Trash") try archive.saveConfiguration(ArchiveConfiguration(accounts: [account], folders: [account.id: [inbox, trash]], recipients: [])) let raw = Data("Subject: Test\r\n\r\nBody".utf8) try archive.store(raw: raw, folderID: inbox.id, validity: "7", uid: "10", flags: []) let store = MailStore(archive: archive) let id = inbox.id + ":7:10" try archive.applyMessageChange(id: id, destination: trash.id, assigned: .init(validity: "8", uid: "50")) try store.applyIdleChange(accountID: account.id, change: .flags(validity: "7", uid: 10, flags: ["\\Deleted"])) try store.applyIdleChange(accountID: account.id, change: .removed(validity: "7", uid: 10)) #expect(store.archiveRevision == 0) #expect(try archive.messages(folderID: inbox.id).isEmpty) #expect(try archive.messages(folderID: trash.id).count == 1) #expect(!store.isBusy) // Changes from another client still update the archive without a network refresh. try archive.store(raw: raw, folderID: inbox.id, validity: "7", uid: "20", flags: []) try store.applyIdleChange(accountID: account.id, change: .flags(validity: "7", uid: 20, flags: ["\\Seen"])) #expect(try archive.messages(folderID: inbox.id).first?.isRead == true) try store.applyIdleChange(accountID: account.id, change: .removed(validity: "7", uid: 20)) #expect(try archive.messages(folderID: inbox.id).isEmpty) #expect(store.archiveRevision == 2) } @Test func queuesPasswordPromptsPerAccount() throws { let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) defer { try? FileManager.default.removeItem(at: root) } let archive = try MailArchive(root: root) let a = MailAccount(name: "A"), b = MailAccount(name: "B") try archive.saveConfiguration(ArchiveConfiguration(accounts: [a, b], folders: [:], recipients: [])) let store = MailStore(archive: archive) store.requestPassword(accountID: a.id) store.requestPassword(accountID: a.id) store.requestPassword(accountID: b.id) #expect(store.authenticationAccount?.id == a.id) store.dismissPasswordRequest() #expect(store.authenticationAccount?.id == b.id) store.dismissPasswordRequest() #expect(store.authenticationAccount == nil) store.requestPassword(accountID: UUID()) #expect(store.authenticationAccount == nil) } }