summaryrefslogtreecommitdiff
path: root/HutchTests
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-19 15:18:38 -0500
committerChristian Cleberg <[email protected]>2026-03-19 15:18:38 -0500
commit659c76df9ff3926cb68886c16167808c23bd8f75 (patch)
tree572bef546fcca19ad37bc1aa7cfb153eb2bf8eb7 /HutchTests
parented48e72520c51e4635cd3840dbc569bdd74c950f (diff)
parent6ba4e967d5dfb5d3c7bb97a0f2662f3180595563 (diff)
downloadhutch-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.swift67
-rw-r--r--HutchTests/InboxViewModelTests.swift168
-rw-r--r--HutchTests/ProjectTests.swift72
-rw-r--r--HutchTests/RepositoryListViewModelTests.swift26
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,