summaryrefslogtreecommitdiff
path: root/HutchTests
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-13 19:44:44 -0500
committerChristian Cleberg <[email protected]>2026-04-13 19:44:44 -0500
commitfe1ccc69661603d419a642a450e4ac9e1258adb0 (patch)
tree386455b6b5468abdd9dfa46dd4802b10da8b77a2 /HutchTests
parent6ec9754ce34f331641b000eddef3d44bca2631a5 (diff)
downloadhutch-fe1ccc69661603d419a642a450e4ac9e1258adb0.tar.gz
hutch-fe1ccc69661603d419a642a450e4ac9e1258adb0.tar.bz2
hutch-fe1ccc69661603d419a642a450e4ac9e1258adb0.zip
fix: centralize URLs, tighten models, and polish SwiftUI bindingsv3.1.3
- Add shared HutchDeepLinkURL constants for app, widgets, and tests. - Bump marketing version to 3.1.2 and build to 70 (app + widget extension). - Point Hutch stats default base URL at HutchStatsAPI; add SRHTWebURL status incident feed and reuse it from SystemStatusService. - Group Project into Metadata and Resources; split ContributionStatsResponse into StatsWindow and StatsTotals with updated decoding and tests. - Replace @Bindable usage with explicit Bindings in Profile, Projects list, and repository ACL flows; simplify Home pinned-item helper; add no-op bodies on cancel alert buttons where the compiler requires a statement. - Move repository row build-status indicator next to the relative-updated caption and reserve a fixed 8×8 slot so the row does not jump when status loads. - Use NSString.lastPathComponent for build artifact filenames; collapse duplicate ACL error branches; minor HutchStats HTTP and XMLParserDelegate cleanups. - Point widgets at HutchDeepLinkURL helpers; align tests with the new response and URL types.
Diffstat (limited to 'HutchTests')
-rw-r--r--HutchTests/ContributionCalendarTests.swift108
-rw-r--r--HutchTests/DeepLinkTests.swift26
-rw-r--r--HutchTests/ProjectPinStoreTests.swift4
-rw-r--r--HutchTests/ProjectTests.swift135
-rw-r--r--HutchTests/SRHTWebURLTests.swift26
-rw-r--r--HutchTests/SystemStatusServiceTests.swift5
6 files changed, 171 insertions, 133 deletions
diff --git a/HutchTests/ContributionCalendarTests.swift b/HutchTests/ContributionCalendarTests.swift
index 9e76e42..0328ad5 100644
--- a/HutchTests/ContributionCalendarTests.swift
+++ b/HutchTests/ContributionCalendarTests.swift
@@ -259,12 +259,12 @@ private final class MockContributionCalendarService: ContributionCalendarServing
self.statsResponses = statsResponses
}
- func fetchContributionCalendar(actor: String, endingOn endDate: Date) async throws -> ContributionCalendarResponse {
+ func fetchContributionCalendar(actor _: String, endingOn _: Date) async throws -> ContributionCalendarResponse {
fetchCalendarCallCount += 1
return calendarResponses[min(fetchCalendarCallCount - 1, calendarResponses.count - 1)]
}
- func fetchContributionStats(actor: String, endingOn endDate: Date) async throws -> ContributionStatsResponse {
+ func fetchContributionStats(actor _: String, endingOn _: Date) async throws -> ContributionStatsResponse {
fetchStatsCallCount += 1
return statsResponses[min(fetchStatsCallCount - 1, statsResponses.count - 1)]
}
@@ -348,65 +348,81 @@ private extension ContributionCalendarResponse {
private extension ContributionStatsResponse {
static func empty(actor: String, year: Int) -> Self {
ContributionStatsResponse(
- actor: actor,
- from: ContributionDateParser.parse("\(year)-01-01")!,
- to: ContributionDateParser.parse("\(year)-01-07")!,
- isIndexed: true,
- lastPolledAt: ContributionDateParser.parseTimestamp("\(year)-01-07T12:00:00Z"),
- indexingState: .indexed,
- totalEvents: 0,
- totalScore: 0,
- activeDays: 0,
- longestStreak: 0,
- currentStreak: 0
+ window: .init(
+ actor: actor,
+ from: ContributionDateParser.parse("\(year)-01-01")!,
+ to: ContributionDateParser.parse("\(year)-01-07")!,
+ isIndexed: true,
+ lastPolledAt: ContributionDateParser.parseTimestamp("\(year)-01-07T12:00:00Z"),
+ indexingState: .indexed
+ ),
+ totals: .init(
+ totalEvents: 0,
+ totalScore: 0,
+ activeDays: 0,
+ longestStreak: 0,
+ currentStreak: 0
+ )
)
}
static func active(actor: String, year: Int, totalEvents: Int, activeDays: Int, longestStreak: Int) -> Self {
ContributionStatsResponse(
- actor: actor,
- from: ContributionDateParser.parse("\(year)-01-01")!,
- to: ContributionDateParser.parse("\(year)-01-07")!,
- isIndexed: true,
- lastPolledAt: ContributionDateParser.parseTimestamp("\(year)-01-07T12:00:00Z"),
- indexingState: .indexed,
- totalEvents: totalEvents,
- totalScore: Double(totalEvents),
- activeDays: activeDays,
- longestStreak: longestStreak,
- currentStreak: 0
+ window: .init(
+ actor: actor,
+ from: ContributionDateParser.parse("\(year)-01-01")!,
+ to: ContributionDateParser.parse("\(year)-01-07")!,
+ isIndexed: true,
+ lastPolledAt: ContributionDateParser.parseTimestamp("\(year)-01-07T12:00:00Z"),
+ indexingState: .indexed
+ ),
+ totals: .init(
+ totalEvents: totalEvents,
+ totalScore: Double(totalEvents),
+ activeDays: activeDays,
+ longestStreak: longestStreak,
+ currentStreak: 0
+ )
)
}
static func pending(actor: String, year: Int) -> Self {
ContributionStatsResponse(
- actor: actor,
- from: ContributionDateParser.parse("\(year)-01-01")!,
- to: ContributionDateParser.parse("\(year)-01-07")!,
- isIndexed: false,
- lastPolledAt: nil,
- indexingState: .pending,
- totalEvents: 0,
- totalScore: 0,
- activeDays: 0,
- longestStreak: 0,
- currentStreak: 0
+ window: .init(
+ actor: actor,
+ from: ContributionDateParser.parse("\(year)-01-01")!,
+ to: ContributionDateParser.parse("\(year)-01-07")!,
+ isIndexed: false,
+ lastPolledAt: nil,
+ indexingState: .pending
+ ),
+ totals: .init(
+ totalEvents: 0,
+ totalScore: 0,
+ activeDays: 0,
+ longestStreak: 0,
+ currentStreak: 0
+ )
)
}
static func error(actor: String, year: Int) -> Self {
ContributionStatsResponse(
- actor: actor,
- from: ContributionDateParser.parse("\(year)-01-01")!,
- to: ContributionDateParser.parse("\(year)-01-07")!,
- isIndexed: false,
- lastPolledAt: nil,
- indexingState: .error,
- totalEvents: 0,
- totalScore: 0,
- activeDays: 0,
- longestStreak: 0,
- currentStreak: 0
+ window: .init(
+ actor: actor,
+ from: ContributionDateParser.parse("\(year)-01-01")!,
+ to: ContributionDateParser.parse("\(year)-01-07")!,
+ isIndexed: false,
+ lastPolledAt: nil,
+ indexingState: .error
+ ),
+ totals: .init(
+ totalEvents: 0,
+ totalScore: 0,
+ activeDays: 0,
+ longestStreak: 0,
+ currentStreak: 0
+ )
)
}
}
diff --git a/HutchTests/DeepLinkTests.swift b/HutchTests/DeepLinkTests.swift
index 12f759b..c57cfce 100644
--- a/HutchTests/DeepLinkTests.swift
+++ b/HutchTests/DeepLinkTests.swift
@@ -6,61 +6,61 @@ struct DeepLinkTests {
@Test
func parsesHomeLink() {
- let link = DeepLink(url: URL(string: "hutch://home")!)
+ let link = DeepLink(url: HutchDeepLinkURL.home)
#expect(link == .home)
}
@Test
func parsesNilPathAsHome() {
- let link = DeepLink(url: URL(string: "hutch://")!)
+ let link = DeepLink(url: HutchDeepLinkURL.emptyHost)
#expect(link == .home)
}
@Test
func parsesRepositoryLink() {
- let link = DeepLink(url: URL(string: "hutch://git/~user/repo")!)
+ let link = DeepLink(url: HutchDeepLinkURL.repositoryGit)
#expect(link == .repository(owner: "~user", repo: "repo"))
}
@Test
func parsesTicketLink() {
- let link = DeepLink(url: URL(string: "hutch://todo/~owner/tracker/42")!)
+ let link = DeepLink(url: HutchDeepLinkURL.ticket)
#expect(link == .ticket(owner: "~owner", tracker: "tracker", ticketId: 42))
}
@Test
func parsesBuildJobLink() {
- let link = DeepLink(url: URL(string: "hutch://builds/12345")!)
+ let link = DeepLink(url: HutchDeepLinkURL.buildJob)
#expect(link == .build(jobId: 12345))
}
@Test
func parsesBuildsTabLink() {
- let link = DeepLink(url: URL(string: "hutch://builds")!)
+ let link = DeepLink(url: HutchDeepLinkURL.builds)
#expect(link == .buildsTab)
}
@Test
func parsesRepositoriesTabLink() {
- let link = DeepLink(url: URL(string: "hutch://repositories")!)
+ let link = DeepLink(url: HutchDeepLinkURL.repositories)
#expect(link == .repositoriesTab)
}
@Test
func parsesTrackersTabLink() {
- let link = DeepLink(url: URL(string: "hutch://trackers")!)
+ let link = DeepLink(url: HutchDeepLinkURL.trackers)
#expect(link == .trackersTab)
}
@Test
func parsesSystemStatusLink() {
- let link = DeepLink(url: URL(string: "hutch://status")!)
+ let link = DeepLink(url: HutchDeepLinkURL.status)
#expect(link == .systemStatus)
}
@Test
func parsesLookupLink() {
- let link = DeepLink(url: URL(string: "hutch://lookup")!)
+ let link = DeepLink(url: HutchDeepLinkURL.lookup)
#expect(link == .lookup)
}
@@ -72,19 +72,19 @@ struct DeepLinkTests {
@Test
func rejectsUnknownPath() {
- let link = DeepLink(url: URL(string: "hutch://unknown")!)
+ let link = DeepLink(url: HutchDeepLinkURL.unknown)
#expect(link == nil)
}
@Test
func rejectsTicketLinkWithNonNumericId() {
- let link = DeepLink(url: URL(string: "hutch://todo/~owner/tracker/abc")!)
+ let link = DeepLink(url: HutchDeepLinkURL.invalidTicketId)
#expect(link == nil)
}
@Test
func rejectsBuildLinkWithNonNumericId() {
- let link = DeepLink(url: URL(string: "hutch://builds/abc")!)
+ let link = DeepLink(url: HutchDeepLinkURL.invalidBuildId)
#expect(link == nil)
}
}
diff --git a/HutchTests/ProjectPinStoreTests.swift b/HutchTests/ProjectPinStoreTests.swift
index 17b4397..13df539 100644
--- a/HutchTests/ProjectPinStoreTests.swift
+++ b/HutchTests/ProjectPinStoreTests.swift
@@ -27,11 +27,11 @@ struct ProjectPinStoreTests {
}
@Test
- func loadPinnedProjectsNormalizesWhitespaceAndDuplicates() {
+ func loadPinnedProjectsNormalizesWhitespaceAndDuplicates() throws {
let defaults = UserDefaults(suiteName: #function)!
defaults.removePersistentDomain(forName: #function)
- let encoded = try! JSONEncoder().encode([
+ let encoded = try JSONEncoder().encode([
"~alice": [" project-1 ", "", "project-1", "project-2"]
])
defaults.set(encoded, forKey: AppStorageKeys.pinnedHomeProjects)
diff --git a/HutchTests/ProjectTests.swift b/HutchTests/ProjectTests.swift
index eff800f..0323f25 100644
--- a/HutchTests/ProjectTests.swift
+++ b/HutchTests/ProjectTests.swift
@@ -3,52 +3,61 @@ import Testing
@testable import Hutch
struct ProjectTests {
+ private enum Fixture {
+ static let exampleWebsite = "https://example.com"
+ }
+
@Test
func resourceSummaryIncludesCounts() {
let project = Project(
- id: "project-1",
- name: "Hutch",
- description: nil,
- website: nil,
- visibility: .public,
- tags: [],
- updated: Date(timeIntervalSince1970: 0),
- 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")
- )
- ]
+ metadata: .init(
+ id: "project-1",
+ name: "Hutch",
+ description: nil,
+ website: nil,
+ visibility: .public,
+ tags: [],
+ updated: Date(timeIntervalSince1970: 0)
+ ),
+ resources: .init(
+ 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")
+ )
+ ],
+ isFullyLoaded: true
+ )
)
#expect(project.resourceSummary == "2 repos • 1 tracker • 1 list")
@@ -57,16 +66,16 @@ struct ProjectTests {
@Test
func resourceSummaryFallsBackToWebsite() {
let project = Project(
- id: "project-1",
- name: "Docs",
- description: nil,
- website: "https://example.com",
- visibility: .public,
- tags: [],
- updated: Date(timeIntervalSince1970: 0),
- mailingLists: [],
- sources: [],
- trackers: []
+ metadata: .init(
+ id: "project-1",
+ name: "Docs",
+ description: nil,
+ website: Fixture.exampleWebsite,
+ visibility: .public,
+ tags: [],
+ updated: Date(timeIntervalSince1970: 0)
+ ),
+ resources: .init(mailingLists: [], sources: [], trackers: [], isFullyLoaded: true)
)
#expect(project.resourceSummary == "Website linked")
@@ -75,16 +84,16 @@ struct ProjectTests {
@Test
func displayHelpersNormalizeBlankValues() {
let project = Project(
- id: "project-1",
- name: " ",
- description: "\n",
- website: "https://example.com",
- visibility: .unlisted,
- tags: [" docs ", "", "Docs", "ios"],
- updated: Date(timeIntervalSince1970: 0),
- mailingLists: [],
- sources: [],
- trackers: []
+ metadata: .init(
+ id: "project-1",
+ name: " ",
+ description: "\n",
+ website: Fixture.exampleWebsite,
+ visibility: .unlisted,
+ tags: [" docs ", "", "Docs", "ios"],
+ updated: Date(timeIntervalSince1970: 0)
+ ),
+ resources: .init(mailingLists: [], sources: [], trackers: [], isFullyLoaded: true)
)
#expect(project.displayName == "Untitled Project")
diff --git a/HutchTests/SRHTWebURLTests.swift b/HutchTests/SRHTWebURLTests.swift
index a183f00..c1f0dc0 100644
--- a/HutchTests/SRHTWebURLTests.swift
+++ b/HutchTests/SRHTWebURLTests.swift
@@ -3,6 +3,16 @@ import Testing
@testable import Hutch
struct SRHTWebURLTests {
+ private enum Expected {
+ static let chatOrigin = "https://chat.sr.ht"
+ static let statusOrigin = "https://status.sr.ht"
+ static let gitRepoHTTPS = "https://git.sr.ht/~ccleberg/hutch"
+ static let gitSSH = "[email protected]:~ccleberg/hutch"
+ static let tracker = "https://todo.sr.ht/~ccleberg/todo"
+ static let ticket = "https://todo.sr.ht/~ccleberg/todo/42"
+ static let buildJob = "https://builds.sr.ht/~ccleberg/job/12"
+ }
+
private let repository = RepositorySummary(
id: 1,
rid: "repo-1",
@@ -26,21 +36,21 @@ struct SRHTWebURLTests {
@Test
func browserOnlyServiceURLsUseCanonicalHosts() {
- #expect(SRHTWebURL.chat.absoluteString == "https://chat.sr.ht")
- #expect(SRHTWebURL.status.absoluteString == "https://status.sr.ht")
+ #expect(SRHTWebURL.chat.absoluteString == Expected.chatOrigin)
+ #expect(SRHTWebURL.status.absoluteString == Expected.statusOrigin)
}
@Test
func repositoryAndCloneURLsUseStableUserScopedPaths() {
- #expect(SRHTWebURL.repository(repository)?.absoluteString == "https://git.sr.ht/~ccleberg/hutch")
- #expect(SRHTWebURL.httpsCloneURL(repository) == "https://git.sr.ht/~ccleberg/hutch")
- #expect(SRHTWebURL.sshCloneURL(repository) == "[email protected]:~ccleberg/hutch")
+ #expect(SRHTWebURL.repository(repository)?.absoluteString == Expected.gitRepoHTTPS)
+ #expect(SRHTWebURL.httpsCloneURL(repository) == Expected.gitRepoHTTPS)
+ #expect(SRHTWebURL.sshCloneURL(repository) == Expected.gitSSH)
}
@Test
func trackerTicketAndBuildURLsUseStableUserScopedPaths() {
- #expect(SRHTWebURL.tracker(tracker)?.absoluteString == "https://todo.sr.ht/~ccleberg/todo")
- #expect(SRHTWebURL.ticket(ownerUsername: "ccleberg", trackerName: "todo", ticketId: 42)?.absoluteString == "https://todo.sr.ht/~ccleberg/todo/42")
- #expect(SRHTWebURL.build(jobId: 12, ownerCanonicalName: "~ccleberg")?.absoluteString == "https://builds.sr.ht/~ccleberg/job/12")
+ #expect(SRHTWebURL.tracker(tracker)?.absoluteString == Expected.tracker)
+ #expect(SRHTWebURL.ticket(ownerUsername: "ccleberg", trackerName: "todo", ticketId: 42)?.absoluteString == Expected.ticket)
+ #expect(SRHTWebURL.build(jobId: 12, ownerCanonicalName: "~ccleberg")?.absoluteString == Expected.buildJob)
}
}
diff --git a/HutchTests/SystemStatusServiceTests.swift b/HutchTests/SystemStatusServiceTests.swift
index d1577b6..d963fa4 100644
--- a/HutchTests/SystemStatusServiceTests.swift
+++ b/HutchTests/SystemStatusServiceTests.swift
@@ -3,6 +3,9 @@ import Testing
@testable import Hutch
struct SystemStatusServiceTests {
+ private enum Fixture {
+ static let ddosIssueURL = "https://status.sr.ht/issues/2026-04-06-ddos-attack/"
+ }
@Test
func parsesCurrentStatusHTMLIntoServicesAndActiveIncidents() throws {
@@ -16,7 +19,7 @@ struct SystemStatusServiceTests {
#expect(snapshot.activeIncidents.count == 1)
#expect(snapshot.activeIncidents[0].title == "SourceHut disrupted due to DDoS attack")
#expect(snapshot.activeIncidents[0].summary == "SourceHut was disrupted by a DDoS attack.")
- #expect(snapshot.activeIncidents[0].url?.absoluteString == "https://status.sr.ht/issues/2026-04-06-ddos-attack/")
+ #expect(snapshot.activeIncidents[0].url?.absoluteString == Fixture.ddosIssueURL)
}
@Test