diff options
| author | Christian Cleberg <[email protected]> | 2026-03-19 15:18:38 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-19 15:18:38 -0500 |
| commit | 659c76df9ff3926cb68886c16167808c23bd8f75 (patch) | |
| tree | 572bef546fcca19ad37bc1aa7cfb153eb2bf8eb7 /HutchTests | |
| parent | ed48e72520c51e4635cd3840dbc569bdd74c950f (diff) | |
| parent | 6ba4e967d5dfb5d3c7bb97a0f2662f3180595563 (diff) | |
| download | hutch-2.tar.gz hutch-2.tar.bz2 hutch-2.zip | |
v2.0: Merge branch 'dev'v2
Diffstat (limited to 'HutchTests')
| -rw-r--r-- | HutchTests/HomeViewModelTests.swift | 67 | ||||
| -rw-r--r-- | HutchTests/InboxViewModelTests.swift | 168 | ||||
| -rw-r--r-- | HutchTests/ProjectTests.swift | 72 | ||||
| -rw-r--r-- | HutchTests/RepositoryListViewModelTests.swift | 26 |
4 files changed, 333 insertions, 0 deletions
diff --git a/HutchTests/HomeViewModelTests.swift b/HutchTests/HomeViewModelTests.swift new file mode 100644 index 0000000..5475bdd --- /dev/null +++ b/HutchTests/HomeViewModelTests.swift @@ -0,0 +1,67 @@ +import Foundation +import Testing +@testable import Hutch + +struct HomeViewModelTests { + + @Test + func failedBuildsKeepsOnlyFailedAndTimedOutJobs() { + let jobs = [ + makeJob(id: 1, status: .success, created: Date(timeIntervalSince1970: 10)), + makeJob(id: 2, status: .failed, created: Date(timeIntervalSince1970: 20)), + makeJob(id: 3, status: .timeout, created: Date(timeIntervalSince1970: 30)), + makeJob(id: 4, status: .running, created: Date(timeIntervalSince1970: 40)) + ] + + let failedBuilds = HomeViewModel.failedBuilds(from: jobs) + + #expect(failedBuilds.map(\.job.id) == [2, 3]) + } + + @Test + func matchesCurrentUserAssigneeNormalizesCanonicalNameAndUsername() { + let currentUser = User( + id: 42, + username: "owner", + canonicalName: "~owner", + email: "[email protected]", + avatar: nil + ) + + #expect(HomeViewModel.matchesCurrentUserAssignee(Entity(canonicalName: "~owner"), currentUser: currentUser)) + #expect(HomeViewModel.matchesCurrentUserAssignee(Entity(canonicalName: "owner"), currentUser: currentUser)) + #expect(HomeViewModel.matchesCurrentUserAssignee(Entity(canonicalName: currentUser.username), currentUser: currentUser)) + #expect(HomeViewModel.matchesCurrentUserAssignee(Entity(canonicalName: "~someone-else"), currentUser: currentUser) == false) + } + + @Test + func primaryRepositoryReferenceParsesManifestSourceURL() { + let manifest = """ + image: alpine/latest + sources: + - https://git.sr.ht/~owner/hutch + tasks: + - true + """ + + let repository = HomeViewModel.primaryRepositoryReference(in: manifest) + + #expect(repository?.ownerCanonicalName == "~owner") + #expect(repository?.name == "hutch") + } + + private func makeJob(id: Int, status: JobStatus, created: Date) -> HomeJobPayload { + HomeJobPayload( + id: id, + created: created, + updated: created, + status: status, + note: nil, + tags: [], + visibility: nil, + image: nil, + tasks: [], + manifest: nil + ) + } +} diff --git a/HutchTests/InboxViewModelTests.swift b/HutchTests/InboxViewModelTests.swift new file mode 100644 index 0000000..b202d03 --- /dev/null +++ b/HutchTests/InboxViewModelTests.swift @@ -0,0 +1,168 @@ +import Foundation +import Testing +@testable import Hutch + +struct InboxViewModelTests { + + @Test + func derivesRepositoryNameFromCommonPatchListSuffixes() { + #expect(InboxViewModel.deriveRepositoryName(from: "hut-devel") == "hut") + #expect(InboxViewModel.deriveRepositoryName(from: "git.patches") == "git") + #expect(InboxViewModel.deriveRepositoryName(from: "discuss") == nil) + } + + @Test + func parsesMailtoDraft() { + let draft = ThreadViewModel.mailComposeDraft( + from: "mailto:[email protected][email protected]&subject=Re:%20PATCH&body=LGTM" + ) + + #expect(draft?.recipients == ["[email protected]"]) + #expect(draft?.ccRecipients == ["[email protected]"]) + #expect(draft?.subject == "Re: PATCH") + #expect(draft?.body == "LGTM") + } + + @Test + func computesLocalUnreadStateFromLastViewedMarker() { + let suiteName = "InboxViewModelTests-\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suiteName)! + defer { defaults.removePersistentDomain(forName: suiteName) } + + let threadID = "list#message" + let lastActivity = Date(timeIntervalSince1970: 2_000) + + #expect(InboxReadStateStore.isUnread(threadID: threadID, lastActivityAt: lastActivity, defaults: defaults)) + + InboxReadStateStore.markViewed(Date(timeIntervalSince1970: 1_000), for: threadID, defaults: defaults) + #expect(InboxReadStateStore.isUnread(threadID: threadID, lastActivityAt: lastActivity, defaults: defaults)) + + InboxReadStateStore.markViewed(Date(timeIntervalSince1970: 2_500), for: threadID, defaults: defaults) + #expect(!InboxReadStateStore.isUnread(threadID: threadID, lastActivityAt: lastActivity, defaults: defaults)) + } + + @Test + func normalizesThreadSubjectsForDisplay() { + let summary = InboxThreadSummary( + rootEmailID: 1, + rootMessageID: "message", + threadRootEmailIDs: [1], + threadRootMessageIDs: ["message"], + listID: 2, + listRID: "list", + listName: "hut-devel", + listOwner: Entity(canonicalName: "~owner"), + subject: "Re: Fwd: [PATCH] test: add parser ", + latestSender: Entity(canonicalName: "~sender"), + lastActivityAt: Date(timeIntervalSince1970: 2_000), + messageCount: 3, + repo: "hut", + containsPatch: true, + isUnread: true + ) + + #expect(summary.displaySubject == "[PATCH] test: add parser") + #expect(summary.metadataLine.contains("~sender")) + #expect(summary.metadataLine.contains("2 replies")) + } + + @Test + func keepsDistinctThreadsDistinctByRootMessageID() { + let baseList = InboxMailingListReference( + id: 1, + rid: "list", + name: "hut-devel", + owner: Entity(canonicalName: "~owner") + ) + + let first = InboxThreadSummary( + rootEmailID: 10, + rootMessageID: "message-1", + threadRootEmailIDs: [10], + threadRootMessageIDs: ["message-1"], + listID: baseList.id, + listRID: baseList.rid, + listName: baseList.name, + listOwner: baseList.owner, + subject: "[PATCH] test", + latestSender: Entity(canonicalName: "~a"), + lastActivityAt: Date(timeIntervalSince1970: 100), + messageCount: 1, + repo: "hut", + containsPatch: true, + isUnread: true + ) + let second = InboxThreadSummary( + rootEmailID: 11, + rootMessageID: "message-2", + threadRootEmailIDs: [11], + threadRootMessageIDs: ["message-2"], + listID: baseList.id, + listRID: baseList.rid, + listName: baseList.name, + listOwner: baseList.owner, + subject: "[PATCH] test", + latestSender: Entity(canonicalName: "~b"), + lastActivityAt: Date(timeIntervalSince1970: 200), + messageCount: 2, + repo: "hut", + containsPatch: true, + isUnread: true + ) + + #expect(first.id != second.id) + } + + @Test + func segmentsPatchBodyAndTreatsSignatureAsPlainText() { + let body = """ + From: Christian Cleberg <[email protected]> + + --- + test-patch.txt | 1 + + 1 file changed, 1 insertion(+) + create mode 100644 test-patch.txt + + diff --git a/test-patch.txt b/test-patch.txt + new file mode 100644 + index 0000000..c7b6eed + --- /dev/null + +++ b/test-patch.txt + @@ -0,0 +1 @@ + +test Wed Mar 18 23:19:03 CDT 2026 + -- + 2.50.1 (Apple Git-155) + """ + + let segments = ThreadViewModel.segmentMessageBodyForTesting(body, isPatch: true) + + #expect(segments.count == 3) + + guard case let .plainText(leadingPlainText) = segments[0] else { + Issue.record("Expected first segment to be plain text") + return + } + #expect(leadingPlainText.contains("From: Christian Cleberg <[email protected]>")) + #expect(leadingPlainText.contains("---")) + #expect(leadingPlainText.contains(" test-patch.txt | 1 +")) + #expect(leadingPlainText.contains(" 1 file changed, 1 insertion(+)")) + #expect(leadingPlainText.contains(" create mode 100644 test-patch.txt")) + + guard case let .diff(diff) = segments[1] else { + Issue.record("Expected second segment to be diff") + return + } + #expect(diff.contains("diff --git a/test-patch.txt b/test-patch.txt")) + #expect(diff.contains("--- /dev/null")) + #expect(diff.contains("+++ b/test-patch.txt")) + #expect(diff.contains("+test Wed Mar 18 23:19:03 CDT 2026")) + #expect(!diff.contains("-- \n2.50.1 (Apple Git-155)")) + + guard case let .plainText(trailingPlainText) = segments[2] else { + Issue.record("Expected third segment to be plain text") + return + } + #expect(trailingPlainText.contains("--")) + #expect(trailingPlainText.contains("2.50.1 (Apple Git-155)")) + } +} diff --git a/HutchTests/ProjectTests.swift b/HutchTests/ProjectTests.swift new file mode 100644 index 0000000..a94d6d2 --- /dev/null +++ b/HutchTests/ProjectTests.swift @@ -0,0 +1,72 @@ +import Foundation +import Testing +@testable import Hutch + +struct ProjectTests { + @Test + func resourceSummaryIncludesCounts() { + let project = Project( + id: "project-1", + name: "Hutch", + description: nil, + website: nil, + visibility: .public, + tags: [], + mailingLists: [ + Project.MailingList( + id: "list-1", + name: "hutch-devel", + description: nil, + visibility: .public, + owner: Entity(canonicalName: "~owner") + ) + ], + sources: [ + Project.SourceRepo( + id: "repo-1", + name: "hutch", + description: nil, + visibility: .public, + owner: Entity(canonicalName: "~owner"), + repoType: .git + ), + Project.SourceRepo( + id: "repo-2", + name: "hutch-web", + description: nil, + visibility: .public, + owner: Entity(canonicalName: "~owner"), + repoType: .git + ) + ], + trackers: [ + Project.Tracker( + id: "tracker-1", + name: "bugs", + description: nil, + visibility: .public, + owner: Entity(canonicalName: "~owner") + ) + ] + ) + + #expect(project.resourceSummary == "2 repos • 1 tracker • 1 list") + } + + @Test + func resourceSummaryFallsBackToWebsite() { + let project = Project( + id: "project-1", + name: "Docs", + description: nil, + website: "https://example.com", + visibility: .public, + tags: [], + mailingLists: [], + sources: [], + trackers: [] + ) + + #expect(project.resourceSummary == "Website linked") + } +} diff --git a/HutchTests/RepositoryListViewModelTests.swift b/HutchTests/RepositoryListViewModelTests.swift index d6e76bf..7bd9126 100644 --- a/HutchTests/RepositoryListViewModelTests.swift +++ b/HutchTests/RepositoryListViewModelTests.swift @@ -28,6 +28,32 @@ struct RepositoryListViewModelTests { #expect(descriptionMatches.map(\.id) == [2]) } + @Test + func buildStatusKeysParsesSourceHutRepositoryURLsFromManifest() { + let manifest = """ + image: alpine/latest + sources: + - https://git.sr.ht/~owner/hutch + - ssh://[email protected]/~owner/wiki + tasks: + - echo "build" + """ + + let keys = RepositoryListViewModel.buildStatusKeys(in: manifest) + + #expect(keys.contains("git|~owner|hutch")) + #expect(keys.contains("hg|~owner|wiki")) + } + + @Test + func repositoryBuildStatusMapsJobStatesToRowStates() { + #expect(RepositoryListViewModel.repositoryBuildStatus(for: .success) == .success) + #expect(RepositoryListViewModel.repositoryBuildStatus(for: .running) == .running) + #expect(RepositoryListViewModel.repositoryBuildStatus(for: .queued) == .running) + #expect(RepositoryListViewModel.repositoryBuildStatus(for: .failed) == .failed) + #expect(RepositoryListViewModel.repositoryBuildStatus(for: .timeout) == .failed) + } + @MainActor private func makeRepository( id: Int, |
